The Pedigree Project 0.1
exit-status-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/PerProcessorScheduler.h"
11#include "pedigree/kernel/process/Process.h"
12#include "pedigree/kernel/process/Scheduler.h"
13#include "pedigree/kernel/process/SignalEvent.h"
14#include "pedigree/kernel/process/Thread.h"
15#include "pedigree/kernel/processor/Processor.h"
16#include "pedigree/kernel/utilities/ZombieQueue.h"
17
18#include <signal.h>
19
20#include "modules/subsys/posix/PosixProcess.h"
21#include "modules/subsys/posix/PosixSubsystem.h"
22#include "modules/subsys/posix/signal-syscalls.h"
23#include "modules/subsys/posix/system-syscalls.h"
24#include <sys/wait.h>
25
26namespace {
27constexpr size_t HostedAttempts = 10000;
28constexpr int NormalExitCode = 0xA5;
29
30struct ExitStatusContext {
31 ExitStatusContext(int code, Subsystem::ExitCause cause) : code(code), cause(cause), entered(0) {}
32
33 int code;
35 Atomic<size_t> entered;
36};
37
38struct TransitionSeederContext {
39 explicit TransitionSeederContext(Process* process)
40 : process(process), ready(0), go(0), done(0), resumes(0) {}
41
42 Process* process;
43 Atomic<size_t> ready;
45 Atomic<size_t> done;
46 Atomic<size_t> resumes;
47};
48
49struct SigchldVisibilityContext {
50 SigchldVisibilityContext()
51 : child(nullptr),
52 childId(0),
53 parentReady(0),
54 releaseParent(0),
55 handlerCalls(0),
56 handlerFailures(0),
57 handlerSawTerminated(0),
58 handlerSawOwnerOnStack(0),
59 waitResult(0),
60 waitStatus(0) {}
61
62 PosixProcess* child;
63 size_t childId;
64 Atomic<size_t> parentReady;
65 Atomic<size_t> releaseParent;
66 Atomic<size_t> handlerCalls;
67 Atomic<size_t> handlerFailures;
68 Atomic<size_t> handlerSawTerminated;
69 Atomic<size_t> handlerSawOwnerOnStack;
70 Atomic<size_t> waitResult;
71 Atomic<size_t> waitStatus;
72};
73
74SigchldVisibilityContext* g_SigchldVisibilityContext = nullptr;
75
76void hostedSigchldHandler(size_t argument) {
77 SigchldVisibilityContext* context =
78 __atomic_load_n(&g_SigchldVisibilityContext, __ATOMIC_ACQUIRE);
79 if (!context) {
80 return;
81 }
82
83 const uint8_t* serializedEvent = reinterpret_cast<const uint8_t*>(argument);
84 if (!serializedEvent || serializedEvent[0] != SIGCHLD || !context->child) {
85 context->handlerFailures += 1;
86 context->handlerCalls += 1;
87 return;
88 }
89
90 context->handlerSawTerminated = context->child->getState() == Process::Terminated ? 1 : 0;
91 context->handlerSawOwnerOnStack = context->child->isTerminationReapableForHostedTest() ? 0 : 1;
92
93 const int status = context->child->getExitStatus();
94 const int result = posix_waitpid(static_cast<int>(context->childId), nullptr, WNOHANG, nullptr);
95 context->waitResult = static_cast<size_t>(result);
96 context->waitStatus = static_cast<size_t>(status);
97 context->handlerCalls += 1;
98}
99
100int sigchldParentTarget(void* parameter) {
101 SigchldVisibilityContext* context = reinterpret_cast<SigchldVisibilityContext*>(parameter);
102 context->parentReady += 1;
103 while (!context->handlerCalls && !context->releaseParent) {
105 }
106 return 0;
107}
108
109int resumeSuspendedProcess(void* parameter) {
110 TransitionSeederContext* context = reinterpret_cast<TransitionSeederContext*>(parameter);
111 context->ready += 1;
112 while (!context->go) {
114 }
115
116 while (!context->done) {
117 if (context->process->isSuspended()) {
118 context->process->resume();
119 context->resumes += 1;
120 return 0;
121 }
123 }
124 return 1;
125}
126
127int deferredPosixExit(void* parameter) {
128 ExitStatusContext* context = reinterpret_cast<ExitStatusContext*>(parameter);
129 context->entered += 1;
130
131 Thread* current = Processor::information().getCurrentThread();
132 if (context->cause == Subsystem::ExitCause::Signal) {
133 pedigree_init_sigret();
134 PosixSubsystem* subsystem = static_cast<PosixSubsystem*>(current->getParent()->getSubsystem());
135 subsystem->sendSignal(current, context->code, false);
136 current->getScheduler()->checkEventState(0);
137 if (current->getUnwindState() != Thread::Exit) {
138 current->deferProcessExit(127);
139 }
140 } else {
141 current->deferProcessExit(context->code);
142 }
144}
145
146bool waitForTermination(Process* process) {
147 for (size_t attempt = 0; attempt < HostedAttempts; ++attempt) {
148 if (process->isTerminationReapableForHostedTest()) {
149 return true;
150 }
152 }
153 return false;
154}
155
156bool runExitStatusFixture(Process* kernelProcess, int code, Subsystem::ExitCause cause,
157 int previousStatus, bool seedTransientStatus) {
158 ExitStatusContext* context = new ExitStatusContext(code, cause);
159 PosixProcess* process = new PosixProcess(kernelProcess);
160 process->setSubsystem(new PosixSubsystem);
161 process->setExitStatus(previousStatus);
162 process->publish();
163
164 bool transientSeeded = !seedTransientStatus;
165 if (seedTransientStatus) {
166 TransitionSeederContext transitionContext(process);
167 Thread* resumer = new Thread(kernelProcess, resumeSuspendedProcess, &transitionContext, nullptr,
168 false, true, true);
169 resumer->setName("hosted POSIX exit-status transition seeder");
170 if (resumer->start()) {
171 for (size_t attempt = 0; attempt < HostedAttempts && !transitionContext.ready; ++attempt) {
173 }
174 if (transitionContext.ready) {
175 transitionContext.go += 1;
176 process->suspend(SIGTSTP);
177 }
178 transitionContext.done += 1;
179 transitionContext.go += 1;
180 transientSeeded = resumer->joinForCompletion() && transitionContext.ready == 1 &&
181 transitionContext.resumes == 1;
182 } else {
183 delete resumer;
184 }
185 }
186
187 Thread* thread = new Thread(process, deferredPosixExit, context, nullptr, false, true, true);
188 thread->setName("hosted POSIX exit-status fixture");
189
190 const bool started = thread->start();
191 if (!started) {
192 delete thread;
193 delete process;
194 delete context;
195 return false;
196 }
197 const bool reapable = waitForTermination(process);
198 const int status = process->getExitStatus();
199 const bool normal = cause == Subsystem::ExitCause::Normal;
200 const int expectedStatus = normal ? ((code & 0xFF) << 8) : (code & 0x7F);
201 const bool statusValid =
202 status == expectedStatus && (normal ? (WIFEXITED(status) && WEXITSTATUS(status) == code)
203 : (WIFSIGNALED(status) && WTERMSIG(status) == code));
204 Process::ChildTransition transition;
205 bool transientCleared = false;
206 {
207 auto guard = kernelProcess->acquireChildStateWait();
208 transientCleared = !process->takePendingChildTransition(true, true, transition);
209 }
210 const bool passed =
211 reapable && context->entered == 1 && transientSeeded && transientCleared && statusValid;
212
213 if (reapable) {
214 delete process;
215 delete context;
216 }
217 return passed;
218}
219
220bool sigchldWaitStatusVisible(Process* kernelProcess) {
221 constexpr int ExitCode = 37;
222 SigchldVisibilityContext context;
223
224 PosixProcess* parent = new PosixProcess(kernelProcess);
225 PosixSubsystem* parentSubsystem = new PosixSubsystem;
226 parent->setSubsystem(parentSubsystem);
227
229 handler->type = 0;
230 handler->pEvent = new SignalEvent(reinterpret_cast<uintptr_t>(&hostedSigchldHandler), SIGCHLD);
231 parentSubsystem->setSignalHandler(SIGCHLD, handler);
232
233 Thread* parentThread =
234 new Thread(parent, sigchldParentTarget, &context, nullptr, false, true, true);
235 parentThread->setName("hosted SIGCHLD wait-status target");
236 parent->publish();
237
238 __atomic_store_n(&g_SigchldVisibilityContext, &context, __ATOMIC_RELEASE);
239 bool parentStarted = parentThread->start();
240 for (size_t attempt = 0; parentStarted && attempt < HostedAttempts && !context.parentReady;
241 ++attempt) {
243 }
244 if (!parentStarted || context.parentReady != 1) {
245 context.releaseParent += 1;
246 if (parentStarted) {
247 parentThread->joinForCompletion();
248 } else {
249 delete parentThread;
250 }
251 __atomic_store_n(&g_SigchldVisibilityContext, static_cast<SigchldVisibilityContext*>(nullptr),
252 __ATOMIC_RELEASE);
253 delete parent;
254 return false;
255 }
256
257 ExitStatusContext childExit(ExitCode, Subsystem::ExitCause::Normal);
258 PosixProcess* child = new PosixProcess(parent);
259 child->setSubsystem(new PosixSubsystem);
260 Thread* childThread =
261 new Thread(child, deferredPosixExit, &childExit, nullptr, false, true, true);
262 childThread->setName("hosted SIGCHLD wait-status child");
263 child->publish();
264 context.child = child;
265 context.childId = child->getId();
266
267 Scheduler::ProcessLease childLease;
268 const bool childLeased = Scheduler::instance().acquireProcess(childLease, child);
269 const bool childStarted = childLeased && childThread->start();
270 if (!childStarted) {
271 delete childThread;
272 childLease.reset();
273 delete child;
274 context.child = nullptr;
275 context.releaseParent += 1;
276 parentThread->joinForCompletion();
277 __atomic_store_n(&g_SigchldVisibilityContext, static_cast<SigchldVisibilityContext*>(nullptr),
278 __ATOMIC_RELEASE);
279 delete parent;
280 return false;
281 }
282
283 for (size_t attempt = 0; childStarted && attempt < HostedAttempts && !context.handlerCalls;
284 ++attempt) {
286 }
287
288 bool reapable = false;
289 for (size_t attempt = 0; attempt < HostedAttempts; ++attempt) {
290 if (child->isTerminationReapableForHostedTest()) {
291 reapable = true;
292 break;
293 }
295 }
296 if (!reapable) {
297 childThread->joinForCompletion();
298 reapable = child->isTerminationReapableForHostedTest();
299 }
300 if (!reapable) {
301 FATAL("SIGCHLD wait-status fixture retained an on-stack child");
302 }
303
304 const int waitStatus = static_cast<int>(static_cast<size_t>(context.waitStatus));
305 bool passed = context.handlerCalls == 1 && context.handlerFailures == 0 &&
306 context.handlerSawTerminated == 1 && context.handlerSawOwnerOnStack == 1 &&
307 context.waitResult == context.childId && WIFEXITED(waitStatus) &&
308 WEXITSTATUS(waitStatus) == ExitCode && reapable &&
309 child->getState() == Process::Reaped;
310
311 Process::ReaperClaim rescueReaper;
312 bool reaperPublished = child->getState() == Process::Reaped;
313 if (!reaperPublished && child->getState() == Process::Terminated) {
314 auto guard = parent->acquireChildStateWait();
315 if (child->getState() == Process::Terminated) {
316 child->reap();
317 rescueReaper = child->tryClaimReaper();
318 }
319 }
320 if (rescueReaper) {
321 rescueReaper.publish();
322 reaperPublished = true;
323 }
324 if (!reaperPublished) {
325 FATAL("SIGCHLD wait-status fixture could not publish child destruction");
326 }
327
328 context.releaseParent += 1;
329 const bool parentJoined = parentStarted && parentThread->joinForCompletion();
330 __atomic_store_n(&g_SigchldVisibilityContext, static_cast<SigchldVisibilityContext*>(nullptr),
331 __ATOMIC_RELEASE);
332
333 Process* childIdentity = child;
334 childLease.reset();
336 passed &= ZombieQueue::instance().drain();
337 passed &= parentJoined;
338 delete parent;
339
340 if (passed) {
341 NOTICE("HOSTED-SYSCALL-TEST: PASS sigchld-wait-status-visible");
342 }
343 return passed;
344}
345} // namespace
346
347bool runHostedPosixExitStatusRegressions(Process* kernelProcess) {
348 const bool normalPassed =
349 runExitStatusFixture(kernelProcess, NormalExitCode, Subsystem::ExitCause::Normal, 0x7F, true);
350 const bool signalPassed =
351 runExitStatusFixture(kernelProcess, SIGUSR1, Subsystem::ExitCause::Signal, 0xFF, false);
352 const bool sigchldPassed = sigchldWaitStatusVisible(kernelProcess);
353
354 if (!normalPassed || !signalPassed || !sigchldPassed) {
355 ERROR(
356 "HOSTED-SYSCALL-TEST: FAIL posix-exit-status: "
357 "termination status or SIGCHLD publication was not wait-compatible");
358 return false;
359 }
360
361 NOTICE("HOSTED-SYSCALL-TEST: PASS posix-normal-exit-status");
362 NOTICE("HOSTED-SYSCALL-TEST: PASS posix-signal-exit-status");
363 return true;
364}
void checkEventState(uintptr_t userStack)
virtual void sendSignal(Thread *pThread, int signal, bool yield=true, bool processDirected=false)
void setSignalHandler(size_t sig, SignalHandler *handler)
void setExitStatus(int code)
Definition Process.h:504
@ 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
WaitQueue::Guard acquireChildStateWait()
Definition Process.h:682
bool takePendingChildTransition(bool includeStopped, bool includeContinued, ChildTransition &transition)
Definition Process.cc:2046
void reap()
Definition Process.cc:1477
void publish()
Definition Process.cc:832
void suspend(int stopSignal=0)
Definition Process.cc:1864
ReaperClaim tryClaimReaper()
Definition Process.cc:1814
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 waitUntilProcessRemoved(Process *expected)
Definition Scheduler.cc:401
void yield()
Definition Scheduler.cc:226
@ Exit
Exit the owning process at the next safe boundary.
Definition Thread.h:514
static void threadExited() NORETURN
Definition Thread.cc:1073
bool joinForCompletion()
Definition Thread.cc:2771
UnwindType getUnwindState()
Definition Thread.h:531
Process * getParent() const
Definition Thread.h:338
class PerProcessorScheduler * getScheduler() const
Definition Thread.h:925
bool start()
Definition Thread.cc:794
void deferProcessExit(int code)
Definition Thread.cc:3653
int type
Type - 0 = normal, 1 = SIG_DFL, 2 = SIG_IGN.
SignalEvent * pEvent
Event for the signal handler.