The Pedigree Project 0.1
process-exit-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/process/Mutex.h"
11#include "pedigree/kernel/process/PerProcessorScheduler.h"
12#include "pedigree/kernel/process/Process.h"
13#include "pedigree/kernel/process/Scheduler.h"
14#include "pedigree/kernel/process/Semaphore.h"
15#include "pedigree/kernel/process/TerminationDeferral.h"
16#include "pedigree/kernel/process/Thread.h"
17#include "pedigree/kernel/utilities/ZombieQueue.h"
18
19namespace {
20bool check(bool condition, const char* detail);
21
22bool schedulerContains(Process* process) {
23 const size_t count = Scheduler::instance().getNumProcesses();
24 for (size_t i = 0; i < count; ++i) {
26 if (Scheduler::instance().acquireProcess(candidate, i) && candidate.get() == process) {
27 return true;
28 }
29 }
30 return false;
31}
32
33bool processLeaseReplacement(Process* process) {
35 bool passed = check(Scheduler::instance().acquireProcess(lease, process),
36 "the process-lease replacement fixture could not acquire its first lease");
37 {
38 TerminationDeferral nestedDeferral;
39 passed &= check(Scheduler::instance().acquireProcess(lease, process),
40 "an active ProcessLease could not be replaced under a newer deferral");
41 passed &= check(lease.get() == process, "ProcessLease replacement changed its target");
42
43 const bool invalidReplacement =
44 Scheduler::instance().acquireProcess(lease, ~static_cast<size_t>(0));
45 passed &= check(!invalidReplacement && !lease,
46 "a failed ProcessLease replacement did not reset the active lease");
47 }
48
49 passed &= check(Scheduler::instance().acquireProcess(lease, process),
50 "ProcessLease could not be reacquired after nested reset");
51 lease.reset();
52
53 if (passed) {
54 NOTICE("HOSTED-WAIT-TEST: PASS process-lease-active-replacement");
55 }
56 return passed;
57}
58
59struct PublicationProbe {
60 explicit PublicationProbe(Process* owner)
61 : visibleDuringMemberConstruction(schedulerContains(owner)) {}
62
63 bool visibleDuringMemberConstruction;
64};
65
66class DeferredHostedProcess : public Process {
67 public:
68 explicit DeferredHostedProcess(Process* parent)
69 : Process(DeferredPublication(), parent), probe(this) {}
70
71 ~DeferredHostedProcess() override {
73 }
74
75 void finishAssembly() {
76 publish();
77 }
78
79 PublicationProbe probe;
80};
81
82struct ProcessExitContext {
83 explicit ProcessExitContext(Mutex* mutex)
84 : mutex(mutex), peerEntered(0), peerAcquiredMutex(0), unstartedPeerEntered(0) {}
85
86 Mutex* mutex;
87 Atomic<size_t> peerEntered;
88 Atomic<size_t> peerAcquiredMutex;
89 Atomic<size_t> unstartedPeerEntered;
90};
91
92struct JoinReaperContext {
93 JoinReaperContext(Process* process, Thread* target)
94 : process(process),
95 target(target),
96 releaseJoiner(0, false),
97 hookCalls(0),
98 hookFailures(0),
99 joinReturned(0),
100 joinSucceeded(0),
101 reaperEntered(0),
102 processDeleted(0) {}
103
104 Process* process;
105 Thread* target;
106 Semaphore releaseJoiner;
107 Atomic<size_t> hookCalls;
108 Atomic<size_t> hookFailures;
109 Atomic<size_t> joinReturned;
110 Atomic<size_t> joinSucceeded;
111 Atomic<size_t> reaperEntered;
112 Atomic<size_t> processDeleted;
113};
114
115JoinReaperContext* g_JoinReaperContext = nullptr;
116
117bool check(bool condition, const char* detail) {
118 if (condition) {
119 return true;
120 }
121
122 ERROR("HOSTED-WAIT-TEST: FAIL process-exit-rendezvous: " << detail);
123 return false;
124}
125
126bool checkJoinLease(bool condition, const char* detail) {
127 if (condition) {
128 return true;
129 }
130
131 ERROR("HOSTED-WAIT-TEST: FAIL join-reaper-lease: " << detail);
132 return false;
133}
134
135int blockedProcessPeer(void* parameter) {
136 ProcessExitContext* context = reinterpret_cast<ProcessExitContext*>(parameter);
137 context->peerEntered += 1;
138 if (context->mutex->acquire()) {
139 context->peerAcquiredMutex += 1;
140 context->mutex->release();
141 }
142 return 0;
143}
144
145int unstartedProcessPeer(void* parameter) {
146 ProcessExitContext* context = reinterpret_cast<ProcessExitContext*>(parameter);
147 context->unstartedPeerEntered += 1;
148 return 0;
149}
150
151int terminateChildProcess(void* parameter) {
152 Process* process = reinterpret_cast<Process*>(parameter);
153 process->kill();
154}
155
156int immediateThreadExit(void*) {
157 return 0;
158}
159
160void joinOperationHook(Thread* target, Process* parent) {
161 JoinReaperContext* context = __atomic_load_n(&g_JoinReaperContext, __ATOMIC_ACQUIRE);
162 if (!context) {
163 return;
164 }
165
166 context->hookCalls += 1;
167 if (target != context->target || parent != context->process) {
168 context->hookFailures += 1;
169 return;
170 }
171
172 if (!context->releaseJoiner.acquireForCompletion()) {
173 context->hookFailures += 1;
174 }
175}
176
177int joinRaceTarget(void* parameter) {
178 JoinReaperContext* context = reinterpret_cast<JoinReaperContext*>(parameter);
179 const bool joined = context->target->join();
180 context->joinSucceeded = joined ? 1 : 0;
181 context->joinReturned += 1;
182 return 0;
183}
184
185int reapJoinProcess(void* parameter) {
186 JoinReaperContext* context = reinterpret_cast<JoinReaperContext*>(parameter);
187 context->reaperEntered += 1;
188 delete context->process;
189 context->processDeleted += 1;
190 return 0;
191}
192
193bool joinReaperLease(Process* kernelProcess) {
194 constexpr size_t Attempts = 10000;
195 Process* process = new Process(kernelProcess);
196
197 Thread* target = new Thread(process, immediateThreadExit, nullptr, nullptr, false, true, true);
198 target->setName("hosted join/reaper target");
199
200 Thread* retained = new Thread(process, immediateThreadExit, nullptr, nullptr, false, true, true);
201 retained->setName("hosted join/reaper retained target");
202
203 const bool targetStarted = target->start();
204 const bool retainedStarted = retained->start();
205 const bool targetsStarted = targetStarted && retainedStarted;
206
207 bool retainedReapable = false;
208 for (size_t attempt = 0; targetsStarted && attempt < Attempts; ++attempt) {
209 if (retained->isReapableForHostedTest()) {
210 retainedReapable = true;
211 break;
212 }
214 }
215
216 bool passed = checkJoinLease(targetsStarted && retainedReapable,
217 "the retained admission target did not become reapable");
218 if (targetsStarted && retainedReapable) {
219 NOTICE("HOSTED-WAIT-TEST: PASS scheduler-same-priority-progress");
220 }
221 if (!retainedReapable) {
222 return false;
223 }
224
225 JoinReaperContext context(process, target);
226 Thread* joiner = new Thread(kernelProcess, joinRaceTarget, &context, nullptr, false, true, true);
227 joiner->setName("hosted join/reaper joiner");
228
229 __atomic_store_n(&g_JoinReaperContext, &context, __ATOMIC_RELEASE);
230 Thread::setJoinOperationHook(joinOperationHook);
231
232 passed &= checkJoinLease(joiner->start(), "the delayed joiner did not start");
233
234 bool joinerPinned = false;
235 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
236 if (context.hookCalls) {
237 joinerPinned = true;
238 break;
239 }
241 }
242 passed &= checkJoinLease(joinerPinned && context.hookFailures == 0,
243 "the joiner did not reach the leased pre-delete window");
244
245 Thread::setJoinOperationHook(nullptr);
246 __atomic_store_n(&g_JoinReaperContext, static_cast<JoinReaperContext*>(nullptr),
247 __ATOMIC_RELEASE);
248
249 if (!joinerPinned) {
250 context.releaseJoiner.release();
251 passed &= checkJoinLease(joiner->join(), "the failed-window joiner did not retire");
252 passed &= checkJoinLease(retained->join(), "the retained target did not retire");
253 delete process;
254 return false;
255 }
256
257 Thread* reaper = new Thread(kernelProcess, reapJoinProcess, &context, nullptr, false, true, true);
258 reaper->setName("hosted join/reaper process reaper");
259 passed &= checkJoinLease(reaper->start(), "the delayed process reaper did not start");
260
261 bool reaperBlocked = false;
262 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
263 Thread::WaitDebugInfo info = {};
264 if (context.reaperEntered && reaper->getWaitDebugInfo(info) && info.queue && info.queued &&
265 info.channelOwner == process && reaper->getStatus() == Thread::Sleeping) {
266 reaperBlocked = true;
267 break;
268 }
270 }
271
272 passed &= checkJoinLease(reaperBlocked && context.processDeleted == 0,
273 "Process destruction did not wait for the active join lease");
274
275 const bool lateJoin = retained->join();
276 passed &= checkJoinLease(!lateJoin, "Process destruction admitted a new join after closing");
277
278 context.releaseJoiner.release();
279
280 bool completed = false;
281 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
282 if (context.joinReturned && context.processDeleted) {
283 completed = true;
284 break;
285 }
287 }
288 passed &= checkJoinLease(completed && context.joinSucceeded == 1,
289 "the join lease did not release into orderly Process destruction");
290 passed &= checkJoinLease(joiner->join(), "the joiner thread did not retire");
291 passed &= checkJoinLease(reaper->join(), "the process reaper thread did not retire");
292
293 if (passed) {
294 NOTICE("HOSTED-WAIT-TEST: PASS join-reaper-lease");
295 }
296 return passed;
297}
298
299class ExitElectionProcess : public Process {
300 public:
301 explicit ExitElectionProcess(Process* parent)
302 : Process(DeferredPublication(), parent),
303 competitor(nullptr),
304 cleanupCalls(0),
305 cleanupBeforePeerReapable(0) {
306 publish();
307 }
308
309 ~ExitElectionProcess() override {
311 }
312
313 Thread* competitor;
314 Atomic<size_t> cleanupCalls;
315 Atomic<size_t> cleanupBeforePeerReapable;
316
317 private:
318 void processTerminated() override {
319 if (competitor && !competitor->isReapableForHostedTest()) {
320 cleanupBeforePeerReapable += 1;
321 }
322 cleanupCalls += 1;
323 }
324};
325
326struct ExitElectionContext {
327 explicit ExitElectionContext(ExitElectionProcess* process)
328 : process(process),
329 releaseCompetitor(0, false),
330 hookCalls(0),
331 hookFailures(0),
332 ownerElected(0),
333 competitorEntered(0),
334 competitorElected(0),
335 resumeSawTerminating(0),
336 owner(nullptr) {}
337
338 ExitElectionProcess* process;
339 Semaphore releaseCompetitor;
340 Atomic<size_t> hookCalls;
341 Atomic<size_t> hookFailures;
342 Atomic<size_t> ownerElected;
343 Atomic<size_t> competitorEntered;
344 Atomic<size_t> competitorElected;
345 Atomic<size_t> resumeSawTerminating;
346 Thread* owner;
347};
348
349ExitElectionContext* g_ExitElectionContext = nullptr;
350
351void exitElectionHook(Process* process, Thread*) {
352 ExitElectionContext* context = __atomic_load_n(&g_ExitElectionContext, __ATOMIC_ACQUIRE);
353 if (!context || process != context->process) {
354 return;
355 }
356
357 context->hookCalls += 1;
358}
359
360int competingProcessExit(void* parameter) {
361 ExitElectionContext* context = reinterpret_cast<ExitElectionContext*>(parameter);
362 if (!context->releaseCompetitor.acquireForCompletion()) {
363 context->hookFailures += 1;
364 }
365
366 // The owner has published Terminating before the hook opens this gate.
367 // A concurrent resume must not restore Active and reopen the process.
368 context->process->resume();
369 context->resumeSawTerminating = context->process->getState() == Process::Terminating ? 1 : 0;
370
371 const bool elected = context->process->beginTermination();
372 context->competitorElected = elected ? 1 : 0;
373 context->competitorEntered += 1;
374 context->process->competitor->getScheduler()->commitCurrentThreadExit();
375}
376
377int owningProcessExit(void* parameter) {
378 ExitElectionContext* context = reinterpret_cast<ExitElectionContext*>(parameter);
379 const bool elected = context->process->beginTermination();
380 context->ownerElected = elected ? 1 : 0;
381 if (!elected || !context->process->quiesceTermination()) {
382 context->hookFailures += 1;
383 context->owner->getScheduler()->commitCurrentThreadExit();
384 }
385 context->process->finishTermination();
386}
387
388bool exitElectionQuiescence(Process* kernelProcess) {
389 ExitElectionProcess* process = new ExitElectionProcess(kernelProcess);
390 ExitElectionContext context(process);
391 Thread* competitor =
392 new Thread(process, competingProcessExit, &context, nullptr, false, true, true);
393 competitor->setName("hosted process-exit competitor");
394 process->competitor = competitor;
395 Thread* owner = new Thread(process, owningProcessExit, &context, nullptr, false, true, true);
396 owner->setName("hosted process-exit owner");
397 context.owner = owner;
398
399 bool passed = check(competitor->start(), "the competing exit thread did not start");
400
401 constexpr size_t Attempts = 10000;
402 bool competitorBlocked = false;
403 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
404 Thread::WaitDebugInfo info = {};
405 if (competitor->getWaitDebugInfo(info) && info.queue && info.queued &&
406 competitor->getStatus() == Thread::Sleeping) {
407 competitorBlocked = true;
408 break;
409 }
411 }
412 passed &= check(competitorBlocked, "the competing exit thread did not publish its election gate");
413
414 __atomic_store_n(&g_ExitElectionContext, &context, __ATOMIC_RELEASE);
415 Process::setTerminationElectionHook(exitElectionHook);
416 passed &= check(owner->start(), "the exit owner did not start");
417
418 bool ownerBlocked = false;
419 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
420 Thread::WaitDebugInfo info = {};
421 if (context.hookCalls == 1 && owner->getWaitDebugInfo(info) && info.queue && info.queued &&
422 owner->getStatus() == Thread::Sleeping) {
423 ownerBlocked = true;
424 break;
425 }
427 }
428 passed &= check(ownerBlocked, "the elected exit owner did not publish its peer rendezvous");
429
430 context.releaseCompetitor.release();
431 const bool reapable = process->waitUntilTerminationReapable();
432 Process::setTerminationElectionHook(nullptr);
433 __atomic_store_n(&g_ExitElectionContext, static_cast<ExitElectionContext*>(nullptr),
434 __ATOMIC_RELEASE);
435
436 passed &= check(reapable && process->getState() == Process::Terminated,
437 "the competing exit process never became reapable");
438 passed &= check(context.hookCalls == 1 && context.hookFailures == 0,
439 "the deterministic election hook did not complete cleanly");
440 passed &= check(
441 context.ownerElected == 1 && context.competitorEntered == 1 && context.competitorElected == 0,
442 "more than one exiting thread won the process election");
443 passed &= check(context.resumeSawTerminating == 1,
444 "a concurrent resume downgraded a terminating process");
445 passed &= check(process->cleanupCalls == 1 && process->cleanupBeforePeerReapable == 0,
446 "shared process cleanup was repeated or ran before peer quiescence");
447 passed &= check(
448 owner->getStatus() == Thread::AwaitingJoin && competitor->getStatus() == Thread::AwaitingJoin,
449 "process cleanup completed before every peer was off-stack");
450
451 delete process;
452 if (passed) {
453 NOTICE("HOSTED-WAIT-TEST: PASS process-exit-election");
454 NOTICE("HOSTED-WAIT-TEST: PASS process-resume-vs-termination");
455 }
456 return passed;
457}
458
459struct CreationDrainContext {
460 explicit CreationDrainContext(ExitElectionProcess* process)
461 : process(process),
462 releaseCreator(0, false),
463 admitted(0),
464 ownerElected(0),
465 quiesced(0),
466 childEntered(0),
467 failures(0),
468 child(nullptr),
469 owner(nullptr) {}
470
471 ExitElectionProcess* process;
472 Semaphore releaseCreator;
473 Atomic<size_t> admitted;
474 Atomic<size_t> ownerElected;
475 Atomic<size_t> quiesced;
476 Atomic<size_t> childEntered;
477 Atomic<size_t> failures;
478 Atomic<Thread*> child;
479 Thread* owner;
480};
481
482int delayedCreationChild(void* parameter) {
483 CreationDrainContext* context = reinterpret_cast<CreationDrainContext*>(parameter);
484 context->childEntered += 1;
485 return 0;
486}
487
488int admittedProcessCreator(void* parameter) {
489 CreationDrainContext* context = reinterpret_cast<CreationDrainContext*>(parameter);
490 Process::ThreadCreationScope creation(*context->process);
491 if (!creation) {
492 context->failures += 1;
493 return 0;
494 }
495 context->admitted = 1;
496 if (!context->releaseCreator.acquireForCompletion() ||
497 context->process->getState() != Process::Terminating) {
498 context->failures += 1;
499 return 0;
500 }
501
502 Thread* child =
503 new Thread(context->process, delayedCreationChild, context, nullptr, false, true, true);
504 if (!child || child->getUnwindState() != Thread::TerminateThread) {
505 context->failures += 1;
506 }
507 context->child = child;
508 return 0;
509}
510
511int creationDrainOwner(void* parameter) {
512 CreationDrainContext* context = reinterpret_cast<CreationDrainContext*>(parameter);
513 const bool elected = context->process->beginTermination();
514 context->ownerElected = elected ? 1 : 0;
515 if (!elected || !context->process->quiesceTermination()) {
516 context->failures += 1;
517 context->owner->getScheduler()->commitCurrentThreadExit();
518 }
519 context->quiesced = 1;
520 context->process->finishTermination();
521}
522
523bool processCreationDrain(Process* kernelProcess) {
524 ExitElectionProcess* process = new ExitElectionProcess(kernelProcess);
525 CreationDrainContext context(process);
526 Thread* creator =
527 new Thread(process, admittedProcessCreator, &context, nullptr, false, true, true);
528 Thread* owner = new Thread(process, creationDrainOwner, &context, nullptr, false, true, true);
529 creator->setName("hosted admitted process creator");
530 owner->setName("hosted creation-drain exit owner");
531 process->competitor = creator;
532 context.owner = owner;
533
534 bool passed = check(creator->start(), "the admitted creator did not start");
535 constexpr size_t Attempts = 10000;
536 bool creatorBlocked = false;
537 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
538 Thread::WaitDebugInfo info = {};
539 if (context.admitted == 1 && creator->getWaitDebugInfo(info) && info.queued &&
540 info.channelOwner == &context.releaseCreator && creator->getStatus() == Thread::Sleeping) {
541 creatorBlocked = true;
542 break;
543 }
545 }
546 passed &= check(creatorBlocked, "the admitted creator did not publish its creation gate");
547
548 {
549 auto reservation = process->reserveTerminalOwner();
550 if (!reservation) {
551 FATAL("The creation-drain fixture could not reserve its terminal owner.");
552 }
553 {
554 Process::ThreadCreationScope denied(*process);
555 passed &= check(!denied, "a terminal-owner reservation admitted a new creator");
556 }
557 reservation.install(owner);
558 }
559 passed &= check(owner->start(), "the creation-drain exit owner did not start");
560
561 bool ownerBlocked = false;
562 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
563 Thread::WaitDebugInfo info = {};
564 uintptr_t address = 0;
565 if (context.ownerElected == 1 && owner->getWaitDebugInfo(info) && info.queued &&
566 owner->getStatus() == Thread::Sleeping &&
567 owner->getDebugState(address) == Thread::ProcessWait &&
568 address == reinterpret_cast<uintptr_t>(process)) {
569 ownerBlocked = true;
570 break;
571 }
573 }
574 passed &= check(ownerBlocked && context.quiesced == 0 && context.child == nullptr,
575 "termination did not wait for the admitted creation before sealing");
576 {
577 Process::ThreadCreationScope denied(*process);
578 passed &= check(!denied, "a terminating process admitted a new creator");
579 }
580
581 context.releaseCreator.release();
582 bool reapable = false;
583 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
584 if (process->isTerminationReapableForHostedTest()) {
585 reapable = true;
586 break;
587 }
589 }
590 passed &= check(reapable, "termination did not finish after the admitted creator was released");
591 if (!reapable) {
592 FATAL("The creation-drain fixture cannot release live worker context.");
593 }
594 Thread* child = context.child;
595 passed &= check(context.failures == 0 && context.admitted == 1 && context.ownerElected == 1 &&
596 context.quiesced == 1 && child && context.childEntered == 0,
597 "late creation failed or its terminal child executed an entry point");
598 passed &=
599 check(process->getState() == Process::Terminated && process->cleanupCalls == 1 &&
600 process->cleanupBeforePeerReapable == 0 && creator->isReapableForHostedTest() &&
601 owner->isReapableForHostedTest() && child && child->isReapableForHostedTest(),
602 "creation-drain cleanup ran before every retained thread was off-stack");
603 delete process;
604 if (passed) {
605 NOTICE("HOSTED-WAIT-TEST: PASS process-creation-termination-drain");
606 }
607 return passed;
608}
609
610struct OrphanExitContext {
611 OrphanExitContext()
612 : process(nullptr),
613 preparingCalls(0),
614 publishedCalls(0),
615 workerEntered(0),
616 workerEnteredBeforeOwnerExit(0),
617 reapableCalls(0),
618 destructorCalls(0),
619 cleanupCalls(0),
620 hookFailures(0),
621 ownerInPublication(0),
622 duplicateClaimsRejected(0) {}
623
624 Process* process;
625 Atomic<size_t> preparingCalls;
626 Atomic<size_t> publishedCalls;
627 Atomic<size_t> workerEntered;
628 Atomic<size_t> workerEnteredBeforeOwnerExit;
629 Atomic<size_t> reapableCalls;
630 Atomic<size_t> destructorCalls;
631 Atomic<size_t> cleanupCalls;
632 Atomic<size_t> hookFailures;
633 Atomic<size_t> ownerInPublication;
634 Atomic<size_t> duplicateClaimsRejected;
635};
636
637OrphanExitContext* g_OrphanExitContext = nullptr;
638
639class OrphanExitProcess : public Process {
640 public:
641 OrphanExitProcess(Process* parent, OrphanExitContext* context)
642 : Process(DeferredPublication(), parent), m_Context(context) {
643 makeOrphanBeforePublicationForHostedTest();
644 publish();
645 }
646
647 ~OrphanExitProcess() override {
649 if (static_cast<size_t>(m_Context->reapableCalls) != 1 || getState() != Process::Terminated) {
650 m_Context->hookFailures += 1;
651 }
652 m_Context->destructorCalls += 1;
653 }
654
655 private:
656 void processTerminated() override {
657 m_Context->cleanupCalls += 1;
658 }
659
660 OrphanExitContext* m_Context;
661};
662
663void orphanPublicationHook(Process* process, Process::OrphanPublicationPhase phase,
664 bool interruptsEnabled, bool processLockHeld) {
665 OrphanExitContext* context = __atomic_load_n(&g_OrphanExitContext, __ATOMIC_ACQUIRE);
666 if (!context || context->process != process) {
667 return;
668 }
669
670 if (!interruptsEnabled || processLockHeld) {
671 context->hookFailures += 1;
672 }
673
674 if (phase == Process::OrphanPublicationPhase::Preparing) {
675 context->preparingCalls += 1;
676 context->ownerInPublication = 1;
677 if (process->getState() != Process::Terminated ||
678 process->isTerminationReapableForHostedTest()) {
679 context->hookFailures += 1;
680 }
681 Process::ReaperClaim duplicate = process->tryClaimReaper();
682 if (duplicate) {
683 context->hookFailures += 1;
684 duplicate.publish();
685 } else {
686 context->duplicateClaimsRejected += 1;
687 }
688 return;
689 }
690
691 context->publishedCalls += 1;
692 constexpr size_t Attempts = 10000;
693 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
694 if (context->workerEntered) {
695 break;
696 }
698 }
699 if (!context->workerEntered) {
700 context->hookFailures += 1;
701 }
702 context->ownerInPublication = 0;
703}
704
705void orphanReapHook(Process* process, ZombieProcess::ReapPhase phase) {
706 OrphanExitContext* context = __atomic_load_n(&g_OrphanExitContext, __ATOMIC_ACQUIRE);
707 if (!context || context->process != process) {
708 return;
709 }
710
711 if (phase == ZombieProcess::ReapPhase::Entered) {
712 context->workerEntered += 1;
713 if (context->ownerInPublication) {
714 context->workerEnteredBeforeOwnerExit += 1;
715 } else {
716 context->hookFailures += 1;
717 }
718 return;
719 }
720
721 context->reapableCalls += 1;
722 if (process->getState() != Process::Terminated) {
723 context->hookFailures += 1;
724 }
725}
726
727bool orphanPublicationInterleaving(Process* kernelProcess) {
728 OrphanExitContext* context = new OrphanExitContext;
729 OrphanExitProcess* process = new OrphanExitProcess(kernelProcess, context);
730 context->process = process;
731
732 __atomic_store_n(&g_OrphanExitContext, context, __ATOMIC_RELEASE);
733 Process::setOrphanPublicationHook(orphanPublicationHook);
734 ZombieProcess::setReapHook(orphanReapHook);
735
736 Thread* owner = new Thread(process, terminateChildProcess, process, nullptr, false, true, true);
737 owner->setName("hosted orphan-exit owner");
738 if (!owner->start()) {
739 Process::setOrphanPublicationHook(nullptr);
740 ZombieProcess::setReapHook(nullptr);
741 __atomic_store_n(&g_OrphanExitContext, static_cast<OrphanExitContext*>(nullptr),
742 __ATOMIC_RELEASE);
743 delete process;
744 delete context;
745 return check(false, "the orphan-exit owner did not start");
746 }
747
748 constexpr size_t Attempts = 20000;
749 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
750 if (context->destructorCalls) {
751 break;
752 }
754 }
755
756 const bool destroyed = context->destructorCalls == 1;
757 const bool drained = destroyed && ZombieQueue::instance().drain();
758 Process::setOrphanPublicationHook(nullptr);
759 ZombieProcess::setReapHook(nullptr);
760 __atomic_store_n(&g_OrphanExitContext, static_cast<OrphanExitContext*>(nullptr),
761 __ATOMIC_RELEASE);
762
763 if (!destroyed || !drained) {
764 // A late Process destructor still owns this diagnostic storage.
765 return check(false, "orphan destruction did not complete and drain");
766 }
767
768 bool passed = true;
769 passed &= check(context->preparingCalls == 1 && context->publishedCalls == 1,
770 "orphan publication did not cross both unlocked checkpoints");
771 passed &= check(context->workerEntered == 1 && context->workerEnteredBeforeOwnerExit == 1,
772 "the ZombieQueue worker did not enter before owner stack retirement");
773 passed &= check(context->reapableCalls == 1 && context->destructorCalls == 1 &&
774 context->cleanupCalls == 1 && context->duplicateClaimsRejected == 1,
775 "orphan destruction was not exactly once and post-reapable");
776 passed &= check(context->hookFailures == 0,
777 "orphan publication violated its status, stack, lock, or interrupt ordering");
778 delete context;
779
780 if (passed) {
781 NOTICE("HOSTED-WAIT-TEST: PASS process-orphan-publication-handoff");
782 }
783 return passed;
784}
785
786struct ZombieBacklogContext {
787 ZombieBacklogContext() : release(0, false), entered(0), destroyed(0), failures(0) {}
788
789 Semaphore release;
790 Atomic<size_t> entered;
791 Atomic<size_t> destroyed;
792 Atomic<size_t> failures;
793};
794
795class HostedBacklogZombie : public ZombieObject {
796 public:
797 explicit HostedBacklogZombie(ZombieBacklogContext* context) : m_Context(context) {}
798
799 ~HostedBacklogZombie() override {
800 m_Context->entered += 1;
801 if (!m_Context->release.acquireForCompletion()) {
802 m_Context->failures += 1;
803 }
804 m_Context->destroyed += 1;
805 }
806
807 private:
808 ZombieBacklogContext* m_Context;
809};
810
811bool mandatoryZombieBacklog() {
812 constexpr size_t Backlog = 300;
813 ZombieBacklogContext context;
814 for (size_t i = 0; i < Backlog; ++i) {
815 ZombieQueue::instance().addObject(new HostedBacklogZombie(&context));
816 }
817
818 bool passed = check(context.destroyed == 0,
819 "mandatory ZombieQueue work executed through its closed test gate");
820 context.release.release(Backlog);
821 passed &= check(ZombieQueue::instance().drain(), "mandatory ZombieQueue backlog did not drain");
822 passed &=
823 check(context.entered == Backlog && context.destroyed == Backlog && context.failures == 0,
824 "mandatory ZombieQueue work above the legacy limit was lost");
825 if (passed) {
826 NOTICE("HOSTED-WAIT-TEST: PASS zombiequeue-mandatory-backlog");
827 }
828 return passed;
829}
830} // namespace
831
832bool runHostedProcessExitRegressions() {
833 Process* kernelProcess = Scheduler::instance().getKernelProcess();
834 Mutex mutex;
835 ProcessExitContext context(&mutex);
836 bool passed = true;
837
838 if (!processLeaseReplacement(kernelProcess)) {
839 return false;
840 }
841
842 if (!joinReaperLease(kernelProcess)) {
843 return false;
844 }
845 passed &= mandatoryZombieBacklog();
846 passed &= orphanPublicationInterleaving(kernelProcess);
847 passed &= exitElectionQuiescence(kernelProcess);
848 passed &= processCreationDrain(kernelProcess);
849
850 DeferredHostedProcess* publishedChild = new DeferredHostedProcess(kernelProcess);
851 passed &= check(!publishedChild->probe.visibleDuringMemberConstruction,
852 "a derived Process was visible while its members were constructing");
853 passed &= check(!schedulerContains(publishedChild),
854 "an incomplete derived Process was visible after its constructor");
855 publishedChild->finishAssembly();
856 passed &=
857 check(schedulerContains(publishedChild), "a complete derived Process was not published");
858 delete publishedChild;
859
860 const size_t processCountBeforeAbandon = Scheduler::instance().getNumProcesses();
861 DeferredHostedProcess* abandonedChild = new DeferredHostedProcess(kernelProcess);
862 delete abandonedChild;
863 passed &= check(Scheduler::instance().getNumProcesses() == processCountBeforeAbandon,
864 "destroying an unpublished Process changed scheduler enumeration");
865
866 Process* terminatingParent = new Process(kernelProcess);
867 terminatingParent->markTerminating();
868 DeferredHostedProcess* freshChild = new DeferredHostedProcess(terminatingParent);
869 freshChild->finishAssembly();
870 passed &= check(freshChild->getState() == Process::Active,
871 "a new child inherited its parent's terminal state");
872 passed &= check(freshChild->getParent() != terminatingParent,
873 "a terminating parent retained a newly published child");
874 delete freshChild;
875 delete terminatingParent;
876
877 passed &= check(mutex.acquire(), "the supervisor could not hold the peer mutex");
878
879 Process* child = new Process(kernelProcess);
880 Process* orphan = new Process(child);
881 Thread* peer = new Thread(child, blockedProcessPeer, &context, nullptr, false, true, true);
882 peer->setName("hosted process-exit blocked peer");
883 Thread* unstartedPeer =
884 new Thread(child, unstartedProcessPeer, &context, nullptr, false, true, true);
885 unstartedPeer->setName("hosted process-exit unstarted peer");
886 Thread* terminator = new Thread(child, terminateChildProcess, child, nullptr, false, true, true);
887 terminator->setName("hosted process-exit terminator");
888
889 passed &= check(peer->start(), "the delayed blocked peer did not start");
890
891 bool peerEnrolled = false;
892 constexpr size_t EnrolmentAttempts = 10000;
893 for (size_t attempt = 0; attempt < EnrolmentAttempts; ++attempt) {
894 Thread::WaitDebugInfo info = {};
895 if (peer->getWaitDebugInfo(info) && info.queue && info.queued &&
896 peer->getStatus() == Thread::Sleeping) {
897 peerEnrolled = true;
898 break;
899 }
901 }
902 passed &=
903 check(context.peerEntered == 1 && peerEnrolled, "the peer did not publish its mutex wait");
904
905 passed &= check(terminator->start(), "the delayed terminating thread did not start");
906 const bool reapable = child->waitUntilTerminationReapable();
907
908 passed &= check(reapable && child->getState() == Process::Terminated,
909 "the child did not publish off-stack termination");
910 passed &= check(peer->getStatus() == Thread::AwaitingJoin &&
911 unstartedPeer->getStatus() == Thread::AwaitingJoin &&
912 terminator->getStatus() == Thread::AwaitingJoin,
913 "not every retained child thread reached AwaitingJoin");
914 passed &=
915 check(context.peerAcquiredMutex == 0, "the blocked peer escaped terminal wait cancellation");
916 passed &= check(context.unstartedPeerEntered == 0,
917 "the delayed unstarted peer executed during process exit");
918 passed &=
919 check(orphan->getParent() != child, "process exit left a child parented to the dead process");
920
921 delete child;
922 delete orphan;
923 mutex.release();
924
925 if (passed) {
926 NOTICE("HOSTED-WAIT-TEST: PASS process-publication-reparent");
927 NOTICE("HOSTED-WAIT-TEST: PASS process-exit-rendezvous");
928 }
929 return passed;
930}
Definition Mutex.h:56
virtual void processTerminated()
Definition Process.h:963
Process * getParent()
Definition Process.h:568
void publish()
Definition Process.cc:832
bool waitUntilTerminationReapable()
Definition Process.cc:2083
void prepareForDestruction()
Definition Process.cc:914
TerminalOwnerReservation reserveTerminalOwner()
Definition Process.cc:1395
ReaperClaim tryClaimReaper()
Definition Process.cc:1814
void kill() NORETURN
Definition Process.cc:1832
static Scheduler & instance()
Definition Scheduler.h:96
size_t getNumProcesses()
Definition Scheduler.cc:257
MUST_USE_RESULT bool acquireProcess(ProcessLease &lease, size_t n)
Definition Scheduler.cc:264
void yield()
Definition Scheduler.cc:226
@ 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
Status getStatus() const
Definition Thread.h:431
UnwindType getUnwindState()
Definition Thread.h:531
bool start()
Definition Thread.cc:794