The Pedigree Project 0.1
child-wait-regressions.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include <config.h>
3
4#if PEDIGREE_CHILD_WAIT_TESTS && THREADS
5#include "pedigree/kernel/Atomic.h"
6#include "pedigree/kernel/Log.h"
7#include "pedigree/kernel/process/Process.h"
8#include "pedigree/kernel/process/Scheduler.h"
9#include "pedigree/kernel/process/Semaphore.h"
10#include "pedigree/kernel/process/Thread.h"
11#include "pedigree/kernel/processor/Processor.h"
12#include "pedigree/kernel/processor/ProcessorInformation.h"
13#include "pedigree/kernel/time/Time.h"
14#include "pedigree/kernel/utilities/ZombieQueue.h"
15
16namespace {
17constexpr size_t TimeoutSeconds = 5;
18constexpr int ExitCode = 37;
19
20bool check(bool condition, const char* detail) {
21 if (!condition) {
22 ERROR("CHILD-WAIT-CORE: FAIL " << detail);
23 }
24 return condition;
25}
26
27bool waitForState(Process& process, Process::ProcessState state) {
28 const auto start = Time::getTicks();
29 while (Time::getTicks() - start < TimeoutSeconds * Time::Multiplier::Second) {
30 if (process.getState() == state) {
31 return true;
32 }
34 }
35 return false;
36}
37
38struct StopContext {
39 Semaphore ready{0}, go{0}, resumed{0};
40 Atomic<bool> abort{false};
41 Atomic<size_t> failures{0};
42};
43
44int stopEntry(void* parameter) {
45 auto& context = *static_cast<StopContext*>(parameter);
46 Process& process = *Processor::information().getCurrentThread()->getParent();
47 for (size_t i = 0; i < 2; ++i) {
48 const size_t epoch = process.getContinuationEpoch();
49 context.ready.release();
50 if (!context.go.acquire(1, TimeoutSeconds)) {
51 context.failures += 1;
52 break;
53 }
54 if (context.abort) {
55 break;
56 }
57 // Cleanup's resume invalidates a stop that has not yet entered its gate.
58 process.suspendIfContinuationEpoch(19 + i, epoch);
59 context.resumed.release();
60 }
61 return 0;
62}
63
64bool selection(Process& parent, Process& child, bool stopped, bool continued, bool consume,
65 Process::ChildTransitionKind expected, int signal = 0) {
66 auto guard = parent.acquireChildStateWait();
67 Process::ChildTransition transition;
68 transition.kind = Process::ChildTransitionKind::Stopped;
69 transition.stopSignal = -1;
70 const bool selected = child.selectPendingChildTransition(stopped, continued, consume, transition);
71 return selected == (expected != Process::ChildTransitionKind::None) &&
72 transition.kind == expected && transition.stopSignal == signal;
73}
74
75bool transitionSelection(Process& parent) {
76 auto* child = new Process(&parent, true);
77 if (!check(child != nullptr, "transition process allocation")) {
78 return false;
79 }
80 StopContext context;
81 auto* worker = new Thread(child, stopEntry, &context, nullptr, false, true, true);
82 if (!worker) {
83 delete child;
84 return check(false, "transition worker allocation");
85 }
86 worker->setName("child wait transition");
87 const bool started = worker->start();
88 using Kind = Process::ChildTransitionKind;
89 bool passed = check(started, "transition worker startup");
90 if (passed) {
91 passed = [&]() {
92 if (!check(context.ready.acquire(1, TimeoutSeconds), "first stop entry")) {
93 return false;
94 }
95 context.go.release();
96 if (!check(waitForState(*child, Process::Suspended), "first stop publication")) {
97 return false;
98 }
99 bool ok = check(selection(parent, *child, false, false, true, Kind::None) &&
100 selection(parent, *child, false, true, true, Kind::None) &&
101 selection(parent, *child, true, false, false, Kind::Stopped, 19) &&
102 selection(parent, *child, true, true, false, Kind::Stopped, 19) &&
103 selection(parent, *child, true, false, true, Kind::Stopped, 19) &&
104 selection(parent, *child, true, true, true, Kind::None) &&
105 child->getState() == Process::Suspended,
106 "stop peek/consume or unmatched-class preservation");
107 child->resume();
108 if (!check(context.resumed.acquire(1, TimeoutSeconds) &&
109 context.ready.acquire(1, TimeoutSeconds),
110 "first resume and second stop entry")) {
111 return false;
112 }
113 ok &= check(selection(parent, *child, true, false, true, Kind::None) &&
114 selection(parent, *child, false, true, false, Kind::Continued) &&
115 selection(parent, *child, true, true, false, Kind::Continued) &&
116 selection(parent, *child, false, true, true, Kind::Continued) &&
117 selection(parent, *child, true, true, false, Kind::None),
118 "continue peek/consume or unmatched-class preservation");
119 context.go.release();
120 if (!check(waitForState(*child, Process::Suspended), "second stop publication")) {
121 return false;
122 }
123 ok &= check(selection(parent, *child, true, false, false, Kind::Stopped, 20),
124 "second stop metadata");
125 child->resume();
126 if (!check(context.resumed.acquire(1, TimeoutSeconds), "second resume")) {
127 return false;
128 }
129 ok &= check(selection(parent, *child, true, false, true, Kind::None) &&
130 selection(parent, *child, false, true, true, Kind::Continued) &&
131 selection(parent, *child, true, true, false, Kind::None),
132 "resume did not replace the unconsumed stop");
133 return ok;
134 }();
135 }
136 context.abort = true;
137 child->resume();
138 context.go.release();
139 if (!started) {
140 worker->setUnwindState(Thread::TerminateThread);
141 }
142 if (!worker->joinForCompletion()) {
143 FATAL("CHILD-WAIT-CORE: transition worker could not retire safely");
144 }
145 passed &= check(context.failures == 0, "transition worker timed out");
146 delete child;
147 if (passed) {
148 NOTICE("CHILD-WAIT-CORE: PASS transition selection");
149 }
150 return passed;
151}
152
153struct TerminalContext {
154 Semaphore ready{0}, finish{0};
155 Atomic<size_t> destroyed{0};
156#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
157 Process* target = nullptr;
158 Atomic<Thread*> reaper{nullptr};
159 Atomic<size_t> reapableCalls{0};
160#endif
161};
162
163class ObservedProcess final : public Process {
164 public:
165 ObservedProcess(Process& parent, TerminalContext& context)
166 : Process(DeferredPublication(), &parent, true), m_Context(context) {
167 publish();
168 }
169 ~ObservedProcess() override {
171 m_Context.destroyed += 1;
172 }
173
174 private:
175 TerminalContext& m_Context;
176};
177
178int exitEntry(void* parameter) {
179 auto& context = *static_cast<TerminalContext*>(parameter);
180 context.ready.release();
181 const bool released = context.finish.acquire(1, TimeoutSeconds);
182 Process& process = *Processor::information().getCurrentThread()->getParent();
183 if (!process.beginTermination(released ? ExitCode : 1) || !process.quiesceTermination()) {
184 FATAL("CHILD-WAIT-CORE: child failed to own its terminal teardown");
185 }
186 // The subsystem normally supplies the encoded status; this is a core-only child.
187 process.setExitStatus((released ? ExitCode : 1) << 8);
188 process.finishTermination();
189}
190
191#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
192TerminalContext* g_TerminalContext = nullptr;
193
194void observeReaper(Process* process, ZombieProcess::ReapPhase phase) {
195 auto* context = __atomic_load_n(&g_TerminalContext, __ATOMIC_ACQUIRE);
196 if (!context || context->target != process) {
197 return;
198 }
199 if (phase == ZombieProcess::ReapPhase::Reapable) {
200 context->reapableCalls += 1;
201 context->reaper = Processor::information().getCurrentThread();
202 }
203}
204
205bool waitForObserverDrain(TerminalContext& context) {
206 const auto start = Time::getTicks();
207 while (Time::getTicks() - start < TimeoutSeconds * Time::Multiplier::Second) {
208 Thread* reaper = context.reaper;
209 Thread::WaitDebugInfo info = {};
210 if (reaper && reaper->getWaitDebugInfo(info) && info.queued &&
211 info.channelOwner == context.target && reaper->getStatus() == Thread::Sleeping) {
212 return true;
213 }
215 }
216 return false;
217}
218#endif
219
220bool terminalObserver(Process& parent) {
221 TerminalContext context;
222 auto* child = new ObservedProcess(parent, context);
223 if (!check(child != nullptr, "terminal process allocation")) {
224 return false;
225 }
226 auto* worker = new Thread(child, exitEntry, &context, nullptr, false, true, true);
227 if (!worker) {
228 delete child;
229 return check(false, "terminal worker allocation");
230 }
231 worker->setName("child wait terminal");
232 if (!worker->start()) {
233 worker->setUnwindState(Thread::TerminateThread);
234 if (!worker->joinForCompletion()) {
235 FATAL("CHILD-WAIT-CORE: failed terminal startup could not retire safely");
236 }
237 delete child;
238 return check(false, "terminal worker startup");
239 }
240 bool passed = check(context.ready.acquire(1, TimeoutSeconds), "terminal worker entry");
241 context.finish.release();
242 if (!waitForState(*child, Process::Terminated)) {
243 FATAL("CHILD-WAIT-CORE: terminal publication timed out");
244 }
245
246 const size_t pid = child->getId();
247 const auto userBefore = parent.getReapedChildrenUserTime();
248 const auto kernelBefore = parent.getReapedChildrenKernelTime();
250 {
251 auto guard = parent.acquireChildStateWait();
252 passed &= check(child->getState() == Process::Terminated &&
253 Scheduler::instance().acquireProcess(observer, child),
254 "terminal observer admission under the child-state guard");
255 }
256 if (!observer || !observer->waitUntilTerminationReapable()) {
257 FATAL("CHILD-WAIT-CORE: terminal observer could not retain an off-stack child");
258 }
259 const int status = observer->getExitStatus();
260 const auto childUser = observer->getUserTime() + observer->getReapedChildrenUserTime();
261 const auto childKernel = observer->getKernelTime() + observer->getReapedChildrenKernelTime();
262 passed &= check(status == (ExitCode << 8) && observer->getState() == Process::Terminated &&
263 parent.getReapedChildrenUserTime() == userBefore &&
264 parent.getReapedChildrenKernelTime() == kernelBefore,
265 "terminal observation consumed status or accounted CPU time");
266
268 {
269 auto guard = parent.acquireChildStateWait();
270 claim = child->tryClaimReaper();
271 if (!claim) {
272 FATAL("CHILD-WAIT-CORE: terminal observer prevented the sole reaper claim");
273 }
274 child->reap();
275 auto duplicate = child->tryClaimReaper();
276 passed &= check(!duplicate, "a competing consumer claimed the child twice");
277 }
278 Time::Timestamp user = 0, kernel = 0;
279 parent.accountReapedChild(child, user, kernel);
280 passed &= check(user == childUser && kernel == childKernel &&
281 parent.getReapedChildrenUserTime() == userBefore + childUser &&
282 parent.getReapedChildrenKernelTime() == kernelBefore + childKernel,
283 "sole consuming claim did not account the final CPU totals once");
284#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
285 context.target = child;
286 __atomic_store_n(&g_TerminalContext, &context, __ATOMIC_RELEASE);
287 ZombieProcess::setReapHook(observeReaper);
288#endif
289 claim.publish();
290
291#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
292 passed &= check(waitForObserverDrain(context) && context.reapableCalls == 1 && !context.destroyed,
293 "published reaper did not wait for the retained observer before destruction");
295 passed &= check(!Scheduler::instance().acquireProcessById(late, pid) && !late,
296 "removed child admitted a new observer");
297 late.reset();
298#else
299 (void)pid;
300 NOTICE("CHILD-WAIT-CORE: SKIP exact reaper wait (hosted hook only)");
301#endif
302 passed &= check(observer->getExitStatus() == status && observer->getState() == Process::Reaped &&
303 !context.destroyed && !observer->tryClaimReaper() &&
304 parent.getReapedChildrenUserTime() == userBefore + childUser &&
305 parent.getReapedChildrenKernelTime() == kernelBefore + childKernel,
306 "retained terminal snapshot or once-only accounting changed after publication");
307 // ProcessLease and ReaperClaim remain on their acquiring thread. Release
308 // the observer outside both state guards so the real reaper can finish.
309 observer.reset();
310 const auto releasedAt = Time::getTicks();
311 while (!context.destroyed &&
312 Time::getTicks() - releasedAt < TimeoutSeconds * Time::Multiplier::Second) {
314 }
315 if (!context.destroyed) {
316 FATAL("CHILD-WAIT-CORE: observer release did not unblock destruction");
317 }
318 const bool drained = ZombieQueue::instance().drain();
319#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
320 ZombieProcess::setReapHook(nullptr);
321 __atomic_store_n(&g_TerminalContext, static_cast<TerminalContext*>(nullptr), __ATOMIC_RELEASE);
322#endif
323 if (!drained) {
324 FATAL("CHILD-WAIT-CORE: child destruction queue did not drain safely");
325 }
326 passed &=
327 check(context.destroyed == 1 && parent.getReapedChildrenUserTime() == userBefore + user &&
328 parent.getReapedChildrenKernelTime() == kernelBefore + kernel,
329 "observer release did not complete exactly one destruction");
330 if (passed) {
331 NOTICE("CHILD-WAIT-CORE: PASS terminal observer and sole reaper");
332 }
333 return passed;
334}
335} // namespace
336
337bool runChildWaitRegressions() {
338 NOTICE("CHILD-WAIT-CORE: BEGIN");
339 auto* parent = new Process(Scheduler::instance().getKernelProcess(), true);
340 if (!check(parent != nullptr, "isolated parent allocation")) {
341 return false;
342 }
343 const bool passed = transitionSelection(*parent) && terminalObserver(*parent);
344 delete parent;
345 if (passed) {
346 NOTICE("CHILD-WAIT-CORE: END PASS");
347 }
348 return passed;
349}
350#endif
void setExitStatus(int code)
Definition Process.h:504
bool beginTermination(int code=0, Subsystem::ExitCause cause=Subsystem::ExitCause::Normal)
Definition Process.cc:1520
ProcessState
Definition Process.h:269
@ Reaped
Terminal wait status is visible; the owner may still be on-stack.
Definition Process.h:274
size_t getId()
Definition Process.h:463
int getExitStatus()
Definition Process.h:508
Process * getParent()
Definition Process.h:568
void accountReapedChild(const Process *child, Time::Timestamp &user, Time::Timestamp &kernel)
Definition Process.cc:758
WaitQueue::Guard acquireChildStateWait()
Definition Process.h:682
void reap()
Definition Process.cc:1477
void publish()
Definition Process.cc:832
bool selectPendingChildTransition(bool includeStopped, bool includeContinued, bool consume, ChildTransition &transition)
Definition Process.cc:2028
void suspendIfContinuationEpoch(int stopSignal, size_t continuationEpoch)
Definition Process.cc:1868
void resume()
Definition Process.cc:1968
bool waitUntilTerminationReapable()
Definition Process.cc:2083
Time::Timestamp getUserTime() const
Definition Process.h:775
void prepareForDestruction()
Definition Process.cc:914
ReaperClaim tryClaimReaper()
Definition Process.cc:1814
size_t getContinuationEpoch()
Definition Process.cc:1963
void finishTermination(bool notifyParent=false) NORETURN
Definition Process.cc:1738
static ProcessorInformation & information()
static Scheduler & instance()
Definition Scheduler.h:96
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
Status getStatus() const
Definition Thread.h:431