The Pedigree Project 0.1
dup3-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/errors.h"
11#include "pedigree/kernel/process/Process.h"
12#include "pedigree/kernel/process/Scheduler.h"
13#include "pedigree/kernel/process/Thread.h"
14#include "pedigree/kernel/processor/Processor.h"
15
16#include <fcntl.h>
17#include <stdint.h>
18
19#include "modules/subsys/posix/FileDescriptor.h"
20#include "modules/subsys/posix/PosixSubsystem.h"
21#include "modules/subsys/posix/eventfd-syscalls.h"
22#include "modules/subsys/posix/file-syscalls.h"
23
24namespace {
25constexpr size_t Attempts = 10000;
26constexpr size_t ReplacementIterations = 256;
27constexpr size_t EventFdRaceIterations = 32;
28constexpr int PreservedErrno = 173;
29
30struct PublicationStressContext {
31 PublicationStressContext(int source, int target)
32 : source(source),
33 target(target),
34 ready(0),
35 start(0),
36 finished(0),
37 observations(0),
38 failures(0) {}
39
40 int source;
41 int target;
42 Atomic<size_t> ready;
43 Atomic<size_t> start;
44 Atomic<size_t> finished;
45 Atomic<size_t> observations;
46 Atomic<size_t> failures;
47};
48
49bool waitFor(const Atomic<size_t>& value, size_t expected) {
50 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
51 if (value.value() == expected) {
52 return true;
53 }
55 }
56 return false;
57}
58
59int replaceWithCloexec(void* parameter) {
60 PublicationStressContext* context = reinterpret_cast<PublicationStressContext*>(parameter);
61 context->ready += 1;
62 if (!waitFor(context->start, 1)) {
63 context->failures += 1;
64 context->finished += 1;
65 return 1;
66 }
67
68 for (size_t iteration = 0; iteration < ReplacementIterations; ++iteration) {
69 if (posix_dup3(context->source, context->target, O_CLOEXEC) != context->target) {
70 context->failures += 1;
71 break;
72 }
74 }
75 context->finished += 1;
76 return 0;
77}
78
79int observeCloexecPublication(void* parameter) {
80 PublicationStressContext* context = reinterpret_cast<PublicationStressContext*>(parameter);
81 context->ready += 1;
82 if (!waitFor(context->start, 1)) {
83 context->failures += 1;
84 return 1;
85 }
86
87 do {
88 if (posix_fcntl(context->target, F_GETFD, nullptr) != FD_CLOEXEC) {
89 context->failures += 1;
90 return 1;
91 }
92 context->observations += 1;
94 } while (!context->finished.value());
95 return 0;
96}
97
98bool cloexecPublicationIsAtomic(Process* process, int source, int target) {
99 PublicationStressContext context(source, target);
100 Thread* replacer = new Thread(process, replaceWithCloexec, &context, nullptr, false, true, true);
101 Thread* observer =
102 new Thread(process, observeCloexecPublication, &context, nullptr, false, true, true);
103 replacer->setName("hosted dup3 CLOEXEC replacer");
104 observer->setName("hosted dup3 CLOEXEC observer");
105 const bool replacerStarted = replacer->start();
106 const bool observerStarted = observer->start();
107 const bool ready = replacerStarted && observerStarted && waitFor(context.ready, 2);
108 if (!replacerStarted) {
109 context.finished += 1;
110 }
111 context.start += 1;
112 const bool replacerJoined = replacerStarted && replacer->joinForCompletion();
113 const bool observerJoined = observerStarted && observer->joinForCompletion();
114 if (!replacerStarted) {
115 delete replacer;
116 }
117 if (!observerStarted) {
118 delete observer;
119 }
120
121 return ready && replacerJoined && observerJoined && context.finished == 1 &&
122 context.observations.value() && !context.failures.value();
123}
124
125struct EventFdCloseRaceContext {
126 EventFdCloseRaceContext(int source, int target)
127 : source(source),
128 target(target),
129 ready(0),
130 start(0),
131 duplicateResult(-2),
132 duplicateError(0),
133 closeResult(-2),
134 failures(0) {}
135
136 int source;
137 int target;
138 Atomic<size_t> ready;
139 Atomic<size_t> start;
140 int duplicateResult;
141 int duplicateError;
142 int closeResult;
143 Atomic<size_t> failures;
144};
145
146int duplicateAgainstFinalClose(void* parameter) {
147 EventFdCloseRaceContext* context = reinterpret_cast<EventFdCloseRaceContext*>(parameter);
148 Thread* thread = Processor::information().getCurrentThread();
149 context->ready += 1;
150 if (!waitFor(context->start, 1)) {
151 context->failures += 1;
152 return 1;
153 }
154 thread->setErrno(0);
155 context->duplicateResult = posix_dup3(context->source, context->target, O_CLOEXEC);
156 context->duplicateError = thread->getErrno();
157 return 0;
158}
159
160int closeEventFdSource(void* parameter) {
161 EventFdCloseRaceContext* context = reinterpret_cast<EventFdCloseRaceContext*>(parameter);
162 context->ready += 1;
163 if (!waitFor(context->start, 1)) {
164 context->failures += 1;
165 return 1;
166 }
167 context->closeResult = posix_close(context->source);
168 return 0;
169}
170
171bool eventFdFinalCloseRace(Process* process, PosixSubsystem* subsystem) {
172 for (size_t iteration = 0; iteration < EventFdRaceIterations; ++iteration) {
173 const int source = posix_eventfd2(1, LinuxEventFd::NonBlock);
174 const int target = posix_eventfd2(2, LinuxEventFd::NonBlock);
175 DescriptorLease sourceDescriptor;
176 DescriptorLease targetDescriptor;
177 if (source < 0 || target < 0 || !subsystem->acquireFileDescriptor(source, sourceDescriptor) ||
178 !subsystem->acquireFileDescriptor(target, targetDescriptor)) {
179 return false;
180 }
182 sourceDescriptor->acquireOpenFileDescription();
184 targetDescriptor->acquireOpenFileDescription();
185 sourceDescriptor.reset();
186 targetDescriptor.reset();
187
188 EventFdCloseRaceContext context(source, target);
189 Thread* duplicator =
190 new Thread(process, duplicateAgainstFinalClose, &context, nullptr, false, true, true);
191 Thread* closer = new Thread(process, closeEventFdSource, &context, nullptr, false, true, true);
192 duplicator->setName("hosted dup3 eventfd duplicator");
193 closer->setName("hosted dup3 eventfd closer");
194 const bool duplicatorStarted = duplicator->start();
195 const bool closerStarted = closer->start();
196 const bool ready = duplicatorStarted && closerStarted && waitFor(context.ready, 2);
197 context.start += 1;
198 const bool duplicatorJoined = duplicatorStarted && duplicator->joinForCompletion();
199 const bool closerJoined = closerStarted && closer->joinForCompletion();
200 if (!duplicatorStarted) {
201 delete duplicator;
202 }
203 if (!closerStarted) {
204 delete closer;
205 }
206
207 DescriptorLease publishedTarget;
208 const bool targetPresent = subsystem->acquireFileDescriptor(target, publishedTarget);
209 const bool duplicateWon = context.duplicateResult == target && context.duplicateError == 0;
210 const bool closeWon =
211 context.duplicateResult == -1 && context.duplicateError == Error::BadFileDescriptor;
212 const bool expectedDescription =
213 targetPresent && publishedTarget->acquireOpenFileDescription().get() ==
214 (duplicateWon ? sourceDescription.get() : targetDescription.get());
215 publishedTarget.reset();
216 const bool targetClosed = posix_close(target) == 0;
217
218 if (!ready || !duplicatorJoined || !closerJoined || context.closeResult ||
219 context.failures.value() || (!duplicateWon && !closeWon) || !expectedDescription ||
220 !targetClosed || sourceDescription->descriptorOwnerCount() ||
221 targetDescription->descriptorOwnerCount()) {
222 return false;
223 }
224 }
225 return true;
226}
227
228struct Dup3Context {
229 explicit Dup3Context(Process* process)
230 : process(process), completed(false), passed(false), returned(0) {}
231
232 Process* process;
233 bool completed;
234 bool passed;
235 Atomic<size_t> returned;
236};
237
238int exerciseDup3(void* parameter) {
239 Dup3Context* context = reinterpret_cast<Dup3Context*>(parameter);
240 Thread* thread = Processor::information().getCurrentThread();
241 PosixSubsystem* subsystem = static_cast<PosixSubsystem*>(thread->getParent()->getSubsystem());
242 bool passed = true;
243
244 const int source = posix_eventfd2(2, LinuxEventFd::NonBlock | LinuxEventFd::CloseOnExec);
245 const int target = posix_eventfd2(7, LinuxEventFd::NonBlock);
246 DescriptorLease sourceDescriptor;
247 DescriptorLease oldTargetDescriptor;
248 passed &= source >= 0 && target >= 0 &&
249 subsystem->acquireFileDescriptor(source, sourceDescriptor) &&
250 subsystem->acquireFileDescriptor(target, oldTargetDescriptor);
252 FileDescriptor::OpenFileDescriptionLease oldTargetDescription;
253 if (sourceDescriptor && oldTargetDescriptor) {
254 sourceDescription = sourceDescriptor->acquireOpenFileDescription();
255 oldTargetDescription = oldTargetDescriptor->acquireOpenFileDescription();
256 }
257 sourceDescriptor.reset();
258 oldTargetDescriptor.reset();
259 if (!sourceDescription || !oldTargetDescription) {
260 if (source >= 0) {
261 posix_close(source);
262 }
263 if (target >= 0) {
264 posix_close(target);
265 }
266 context->completed = true;
267 context->returned += 1;
268 return 1;
269 }
270
271 thread->setErrno(PreservedErrno);
272 passed &= posix_dup3(source, target, 0) == target && thread->getErrno() == PreservedErrno &&
273 sourceDescription->descriptorOwnerCount() == 2 &&
274 oldTargetDescription->descriptorOwnerCount() == 0;
275
276 DescriptorLease duplicate;
277 passed &= subsystem->acquireFileDescriptor(target, duplicate) &&
278 duplicate->acquireOpenFileDescription() == sourceDescription &&
279 duplicate->getFlags() == 0;
280 duplicate.reset();
281
282 uint64_t value = 0;
283 passed &= posix_read(target, reinterpret_cast<char*>(&value), sizeof(value)) == 8 && value == 2;
284 value = 3;
285 passed &= posix_write(source, reinterpret_cast<char*>(&value), sizeof(value), false) == 8;
286 value = 0;
287 passed &= posix_read(target, reinterpret_cast<char*>(&value), sizeof(value)) == 8 && value == 3;
288
289 passed &= posix_dup3(source, target, O_CLOEXEC) == target &&
290 posix_fcntl(target, F_GETFD, nullptr) == FD_CLOEXEC;
291 DescriptorLease retainedTarget;
292 passed &= subsystem->acquireFileDescriptor(target, retainedTarget);
294 if (retainedTarget) {
295 retainedDescription = retainedTarget->acquireOpenFileDescription();
296 }
297 retainedTarget.reset();
298
299 thread->setErrno(0);
300 passed &= posix_dup3(source, source, 0) == -1 && thread->getErrno() == Error::InvalidArgument;
301 thread->setErrno(0);
302 passed &= posix_dup3(-1, -1, 0) == -1 && thread->getErrno() == Error::InvalidArgument;
303 thread->setErrno(0);
304 passed &=
305 posix_dup3(source, target, O_NONBLOCK) == -1 && thread->getErrno() == Error::InvalidArgument;
306 thread->setErrno(0);
307 passed &= posix_dup3(-1, target, 0) == -1 && thread->getErrno() == Error::BadFileDescriptor;
308 thread->setErrno(0);
309 passed &= posix_dup3(source, -1, 0) == -1 && thread->getErrno() == Error::BadFileDescriptor;
310 thread->setErrno(0);
311 passed &= posix_dup3(source, 16384, 0) == -1 && thread->getErrno() == Error::BadFileDescriptor;
312
313 DescriptorLease unchangedTarget;
314 passed &= subsystem->acquireFileDescriptor(target, unchangedTarget) &&
315 unchangedTarget->acquireOpenFileDescription() == retainedDescription &&
316 unchangedTarget->getFlags() == FD_CLOEXEC;
317 unchangedTarget.reset();
318
319 const size_t reserved = subsystem->getFd(100);
320 thread->setErrno(0);
321 passed &= reserved == 100 && posix_dup3(-1, static_cast<int>(reserved), 0) == -1 &&
322 thread->getErrno() == Error::BadFileDescriptor;
323 thread->setErrno(0);
324 passed &= posix_dup3(source, static_cast<int>(reserved), O_CLOEXEC) == -1 &&
325 thread->getErrno() == Error::DeviceBusy;
326 DescriptorLease unpublishedReservation;
327 passed &= !subsystem->acquireFileDescriptor(reserved, unpublishedReservation);
328 subsystem->freeFd(reserved);
329 passed &=
330 posix_dup3(source, static_cast<int>(reserved), O_CLOEXEC) == static_cast<int>(reserved) &&
331 posix_fcntl(static_cast<int>(reserved), F_GETFD, nullptr) == FD_CLOEXEC &&
332 posix_close(static_cast<int>(reserved)) == 0;
333
334 passed &= cloexecPublicationIsAtomic(context->process, source, target);
335 passed &= posix_close(source) == 0;
336 value = 5;
337 passed &= posix_write(target, reinterpret_cast<char*>(&value), sizeof(value), false) == 8;
338 value = 0;
339 passed &= posix_read(target, reinterpret_cast<char*>(&value), sizeof(value)) == 8 && value == 5 &&
340 posix_close(target) == 0 && sourceDescription->descriptorOwnerCount() == 0;
341
342 passed &= eventFdFinalCloseRace(context->process, subsystem);
343 context->passed = passed;
344 context->completed = true;
345 context->returned += 1;
346 return passed ? 0 : 1;
347}
348} // namespace
349
350bool runHostedDup3Regressions(Process* kernelProcess) {
351 Process* process = new Process(kernelProcess);
352 process->setSubsystem(new PosixSubsystem);
353 Dup3Context context(process);
354 Thread* worker = new Thread(process, exerciseDup3, &context, nullptr, false, true, true);
355 worker->setName("hosted dup3 regression worker");
356 const bool started = worker->start();
357 const bool joined = started && worker->joinForCompletion();
358 if (!started) {
359 delete worker;
360 }
361
362 const bool passed =
363 started && joined && context.returned == 1 && context.completed && context.passed;
364 delete process;
365 if (!passed) {
366 ERROR(
367 "HOSTED-SYSCALL-TEST: FAIL dup3-atomic-replacement: "
368 "validation, OFD sharing, CLOEXEC publication, replacement, or eventfd close race "
369 "regressed");
370 return false;
371 }
372
373 NOTICE("HOSTED-SYSCALL-TEST: PASS dup3-atomic-replacement");
374 return true;
375}
OpenFileDescriptionLease acquireOpenFileDescription() const
int getFlags() const
Get current descriptor flags.
bool acquireFileDescriptor(size_t fd, DescriptorLease &descriptor)
size_t getFd(size_t minimum=0)
void freeFd(size_t fdNum)
static ProcessorInformation & information()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
T * get() const
void setErrno(size_t err)
Definition Thread.h:478
size_t getErrno()
Definition Thread.h:473
bool joinForCompletion()
Definition Thread.cc:2771
Process * getParent() const
Definition Thread.h:338
bool start()
Definition Thread.cc:794