The Pedigree Project 0.1
futex-robust-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/process/WaitQueue.h"
15#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
16#include "pedigree/kernel/time/Time.h"
17
18#include <limits.h>
19#include <stddef.h>
20#include <time.h>
21
22#include "modules/subsys/posix/PosixProcess.h"
23#include "modules/subsys/posix/PosixSubsystem.h"
24#include "modules/subsys/posix/pthread-syscalls.h"
25#include "modules/subsys/posix/signal-syscalls.h"
26#include "modules/subsys/posix/system-syscalls.h"
28
29namespace {
30constexpr int PrivateWait = 128;
31constexpr int PrivateWake = 129;
32constexpr int PrivateRequeue = 131;
33constexpr int RealtimeBitsetWait = 393;
34constexpr int PreservedErrno = 173;
35constexpr uint32_t OwnerDied = 0x40000000;
36constexpr uint32_t Waiters = 0x80000000;
37
38struct CrossOwnerContext {
39 WaitQueue queue;
40 size_t firstOwner = 0;
41 size_t secondOwner = 0;
42 bool passed = false;
43};
44CrossOwnerContext* g_CrossOwner = nullptr;
45
46void requeueAcrossOwners(WaitQueue* queue, Thread* thread, const WaitQueue::Channel&, size_t) {
47 CrossOwnerContext* context = g_CrossOwner;
48 if (!context || queue != &context->queue) {
49 return;
50 }
51 WaitQueue::setBeforeBlockHook(nullptr);
52 const WaitQueue::Channel source(&context->firstOwner, 11);
53 const WaitQueue::Channel destination(&context->secondOwner, 29);
54 auto guard = queue->acquire();
55 bool passed = guard.wakeAndRequeue(source, 0, destination, 1) == 1;
56 Thread::WaitDebugInfo info = {};
57 passed &= thread->getWaitDebugInfo(info) && info.queue == queue && info.queued &&
58 info.channelOwner == destination.owner && info.channelValue == destination.value;
59 passed &= !guard.wakeOne(WaitQueue::WakeReason::Signalled, source);
60 passed &= guard.wakeOne(WaitQueue::WakeReason::Signalled, destination);
61 context->passed = passed;
62}
63
64bool crossOwnerRequeue() {
65 CrossOwnerContext context;
66 g_CrossOwner = &context;
67 WaitQueue::setBeforeBlockHook(requeueAcrossOwners);
68 auto guard = context.queue.acquire();
69 const auto reason = guard.wait(WaitQueue::Channel(&context.firstOwner, 11), Thread::FutexWait);
70 WaitQueue::setBeforeBlockHook(nullptr);
71 g_CrossOwner = nullptr;
72 return context.passed && reason == WaitQueue::WakeReason::Signalled &&
73 context.queue.waiterCount() == 0;
74}
75
76struct PeerContext {
77 WaitQueue queue;
78 uintptr_t head = 0;
79 bool release = false;
80 Atomic<size_t> ready{0};
81};
82
83int peerWorker(void* parameter) {
84 PeerContext* context = reinterpret_cast<PeerContext*>(parameter);
85 Thread* thread = Processor::information().getCurrentThread();
86 thread->setRobustList(context->head, thread->getTaskId());
87 context->ready += 1;
88 while (true) {
89 auto guard = context->queue.acquire();
90 if (context->release) {
91 return 0;
92 }
93 const auto reason = guard.wait();
94 (void)reason;
95 }
96}
97
98void releasePeer(PeerContext& context) {
99 auto guard = context.queue.acquire();
100 context.release = true;
101 guard.wakeAll();
102}
103
104bool awaitReady(Atomic<size_t>& ready) {
105 const Time::Timestamp deadline = Time::getTicks() + Time::Multiplier::Second * 2;
106 while (!ready && Time::getTicks() < deadline) {
108 }
109 return ready == 1;
110}
111
112struct RobustNode {
113 uint32_t owner;
114 uint32_t padding;
115 uintptr_t next;
116};
117
118struct UserFixture {
119 uintptr_t next;
120 intptr_t offset;
121 uintptr_t pending;
122 RobustNode held;
123 RobustNode foreign;
124 RobustNode acquiring;
125 uintptr_t peerHead[3];
126 uintptr_t returnedHead;
127 size_t returnedLength;
128 int source;
129 int destination;
130 timespec timeout;
131 timespec zeroTimeout;
132};
133
134struct FutexContext {
135 uintptr_t address;
136 bool passed = false;
137};
138
139struct AbsoluteWaitContext {
140 Thread* thread;
141 UserFixture* fixture;
142 bool entered = false;
143 bool passed = false;
144};
145AbsoluteWaitContext* g_AbsoluteWait = nullptr;
146
147void wakeRequeuedAbsoluteWait(WaitQueue*, Thread* thread, const WaitQueue::Channel&, size_t state) {
148 auto* context = g_AbsoluteWait;
149 if (!context || context->thread != thread || state != Thread::FutexWait)
150 return;
151 WaitQueue::setBeforeBlockHook(nullptr);
152 context->entered = true;
153 auto* fixture = context->fixture;
154 context->passed =
155 posix_futex(&fixture->source, PrivateRequeue, 0, 1, &fixture->destination, 0) == 1;
156 const int changed = 1;
157 context->passed &= PosixSubsystem::copyToUser(&fixture->source, &changed, sizeof(changed));
158 // Retire the queue wait through a clock notification before the actual wake.
159 // The registered futex must retain the second wake and its new identity.
160 posix_futex_clock_changed();
161 context->passed &= posix_futex(&fixture->source, PrivateWake, 1, 0, nullptr, 0) == 0;
162 context->passed &= posix_futex(&fixture->destination, PrivateWake, 1, 0, nullptr, 0) == 1;
163}
164
165int futexWaiter(void* parameter) {
166 FutexContext* context = reinterpret_cast<FutexContext*>(parameter);
167 auto* fixture = reinterpret_cast<UserFixture*>(context->address);
168 context->passed = posix_futex(&fixture->source, PrivateWait, 0,
169 reinterpret_cast<uintptr_t>(&fixture->timeout), nullptr, 0) == 0;
170 return context->passed ? 0 : 1;
171}
172
173struct ContractContext {
174 Thread* foreign;
175 uintptr_t address = 0;
176 uint32_t expectedExitOwner = 0;
177 bool passed = false;
178};
179
180int contractWorker(void* parameter) {
181 ContractContext* context = reinterpret_cast<ContractContext*>(parameter);
182 Thread* thread = Processor::information().getCurrentThread();
183 Process* process = thread->getParent();
184 VirtualAddressSpace* space = process->getAddressSpace();
186 const size_t pageSize = PhysicalMemoryManager::getPageSize();
187 uintptr_t address = 0;
188 if (!process->allocateUserRange(Process::UserRegion::Normal, pageSize, address)) {
189 return 1;
190 }
191 uintptr_t mappedAddress = address;
192 if (!mappings.mapAnon(mappedAddress, pageSize,
193 MemoryMappedObject::Read | MemoryMappedObject::Write) ||
194 mappedAddress != address) {
195 return 1;
196 }
197 context->address = address;
198 auto* fixture = reinterpret_cast<UserFixture*>(address);
199 bool passed = thread->getId() == 1 && context->foreign->getId() == 1 &&
200 thread->getTaskId() == process->getId() &&
201 thread->getTaskId() != context->foreign->getTaskId() &&
202 posix_gettid() == static_cast<pid_t>(thread->getId()) &&
203 posix_gettid(true) == static_cast<pid_t>(thread->getTaskId()) &&
204 posix_set_tid_address(nullptr, true) == static_cast<pid_t>(thread->getTaskId());
205
206 // The first predicate check must fault in the anonymous page, then compare
207 // atomically without enrolling a waiter when the value differs.
208 thread->setErrno(0);
209 passed &= posix_futex(&fixture->source, PrivateWait, 1, 0, nullptr, 0) == -1 &&
210 thread->getErrno() == Error::NoMoreProcesses;
211
212 UserFixture initial = {};
213 initial.next = reinterpret_cast<uintptr_t>(&fixture->held.next);
214 initial.offset = -static_cast<intptr_t>(offsetof(RobustNode, next));
215 initial.pending = reinterpret_cast<uintptr_t>(&fixture->acquiring.next);
216 initial.held = {static_cast<uint32_t>(thread->getTaskId()) | Waiters, 0,
217 reinterpret_cast<uintptr_t>(&fixture->foreign.next)};
218 initial.foreign = {static_cast<uint32_t>(context->foreign->getTaskId()), 0, address};
219 initial.acquiring = {static_cast<uint32_t>(thread->getTaskId()), 0, 0};
220 initial.peerHead[0] = reinterpret_cast<uintptr_t>(fixture->peerHead);
221 initial.timeout.tv_sec = 2;
222 passed &= PosixSubsystem::copyToUser(fixture, &initial, sizeof(initial));
223
224 thread->setErrno(0);
225 passed &= posix_futex(&fixture->source, PrivateWait, 0,
226 reinterpret_cast<uintptr_t>(&fixture->zeroTimeout), nullptr, 0) == -1 &&
227 thread->getErrno() == Error::TimedOut;
228 uint32_t observed = 0;
229 bool exchanged = false;
230 passed &= space->tryReadUser32(reinterpret_cast<uintptr_t>(&fixture->source), observed) &&
231 observed == 0;
232 passed &= mappings.setPermissions(address, pageSize, MemoryMappedObject::Read) == 1;
233 passed &= !space->tryCompareExchangeUser32(reinterpret_cast<uintptr_t>(&fixture->source),
234 observed, 7, exchanged) &&
235 !exchanged;
236 passed &= mappings.setPermissions(address, pageSize, MemoryMappedObject::None) == 1;
237 passed &= !space->tryReadUser32(reinterpret_cast<uintptr_t>(&fixture->source), observed);
238 passed &= mappings.setPermissions(address, pageSize,
239 MemoryMappedObject::Read | MemoryMappedObject::Write) == 1;
240 passed &= space->tryReadUser32(reinterpret_cast<uintptr_t>(&fixture->source), observed) &&
241 observed == 0;
242
243 FutexContext futex{address};
244 Thread* waiter = new Thread(process, futexWaiter, &futex, nullptr, false, true, true);
245 bool waiterStarted = waiter->start();
246 bool enrolled = false;
247 const Time::Timestamp deadline = Time::getTicks() + Time::Multiplier::Second;
248 while (waiterStarted && Time::getTicks() < deadline) {
249 Thread::WaitDebugInfo info = {};
250 if (waiter->getWaitDebugInfo(info) && info.queued && info.channelOwner == space &&
251 info.channelValue == reinterpret_cast<uintptr_t>(&fixture->source)) {
252 enrolled = true;
253 break;
254 }
256 }
257 passed &= enrolled &&
258 posix_futex(&fixture->source, PrivateRequeue, 0, 1, &fixture->destination, 0) == 1;
259 passed &= posix_futex(&fixture->source, PrivateWake, 1, 0, nullptr, 0) == 0;
260 const int destinationWoken = posix_futex(&fixture->destination, PrivateWake, 1, 0, nullptr, 0);
261 passed &= destinationWoken == 1;
262 if (!destinationWoken) {
263 posix_futex(&fixture->source, PrivateWake, 1, 0, nullptr, 0);
264 }
265 const bool waiterJoined = waiterStarted && waiter->joinForCompletion();
266 if (!waiterStarted) {
267 delete waiter;
268 }
269 passed &= waiterJoined && futex.passed;
270
271 const Time::Timestamp future = Time::getTimeNanoseconds() + 10 * Time::Multiplier::Second;
272 const struct timespec absolute = {static_cast<time_t>(future / Time::Multiplier::Second),
273 static_cast<long>(future % Time::Multiplier::Second)};
274 passed &= PosixSubsystem::copyToUser(&fixture->timeout, &absolute, sizeof(absolute));
275 AbsoluteWaitContext absoluteContext{thread, fixture};
276 g_AbsoluteWait = &absoluteContext;
277 const size_t alarmCreates = Time::getHostedAlarmCreateCount();
278 const size_t alarmDestroys = Time::getHostedAlarmDestroyCount();
279 WaitQueue::setBeforeBlockHook(&wakeRequeuedAbsoluteWait);
280 const int absoluteResult = posix_futex(&fixture->source, RealtimeBitsetWait, 0,
281 reinterpret_cast<uintptr_t>(&fixture->timeout), nullptr,
282 static_cast<int>(UINT32_MAX));
283 WaitQueue::setBeforeBlockHook(nullptr);
284 g_AbsoluteWait = nullptr;
285 passed &= absoluteResult == 0 && absoluteContext.entered && absoluteContext.passed &&
286 Time::getHostedAlarmCreateCount() == alarmCreates + 1 &&
287 Time::getHostedAlarmDestroyCount() == alarmDestroys + 1;
288 passed &= posix_futex(&fixture->destination, PrivateWake, 1, 0, nullptr, 0) == 0;
289 posix_futex_clock_changed();
290
291 auto** outputHead = reinterpret_cast<robust_list_head**>(&fixture->returnedHead);
292 thread->setErrno(PreservedErrno);
293 passed &= posix_get_robust_list(0, outputHead, &fixture->returnedLength, true) == 0 &&
294 thread->getErrno() == PreservedErrno && fixture->returnedHead == 0 &&
295 fixture->returnedLength == 3 * sizeof(uintptr_t);
296 thread->setErrno(0);
297 passed &= posix_set_robust_list(reinterpret_cast<robust_list_head*>(address), 0, true) == -1 &&
298 thread->getErrno() == Error::InvalidArgument;
299 passed &= posix_set_robust_list(reinterpret_cast<robust_list_head*>(address),
300 3 * sizeof(uintptr_t), true) == 0;
301
302 PeerContext peer;
303 peer.head = reinterpret_cast<uintptr_t>(fixture->peerHead);
304 Thread* buddy = new Thread(process, peerWorker, &peer, nullptr, false, true, true);
305 const bool buddyStarted = buddy->start();
306 const bool buddyReady = buddyStarted && awaitReady(peer.ready);
307 passed &= buddyReady && buddy->getTaskId() != thread->getTaskId() &&
308 buddy->getTaskId() != context->foreign->getTaskId();
309 passed &= posix_get_robust_list(static_cast<int>(buddy->getTaskId()), outputHead,
310 &fixture->returnedLength, true) == 0 &&
311 fixture->returnedHead == peer.head && thread->getRobustList() == address;
312 thread->setErrno(0);
313 passed &= posix_get_robust_list(static_cast<int>(context->foreign->getTaskId()), outputHead,
314 &fixture->returnedLength, true) == -1 &&
315 thread->getErrno() == Error::NotEnoughPermissions;
316 thread->setErrno(0);
317 passed &= posix_get_robust_list(INT_MAX, outputHead, &fixture->returnedLength, true) == -1 &&
318 thread->getErrno() == Error::NoSuchProcess;
319 thread->setErrno(0);
320 passed &= posix_get_robust_list(0, nullptr, &fixture->returnedLength, true) == -1 &&
321 thread->getErrno() == Error::BadAddress;
322 passed &= posix_tkill(static_cast<int>(buddy->getTaskId()), 0, true) == 0 &&
323 posix_tgkill(static_cast<int>(process->getId()), static_cast<int>(buddy->getTaskId()),
324 0, true) == 0 &&
325 posix_tkill(static_cast<int>(context->foreign->getTaskId()), 0, true) == 0;
326 thread->setErrno(0);
327 passed &= posix_tgkill(static_cast<int>(process->getId()),
328 static_cast<int>(context->foreign->getTaskId()), 0, true) == -1 &&
329 thread->getErrno() == Error::NoSuchProcess;
330 releasePeer(peer);
331 const bool buddyJoined = buddyStarted && buddy->joinForCompletion();
332 if (!buddyStarted) {
333 delete buddy;
334 }
335 passed &= buddyJoined;
336
337 posix_robust_list_exit(thread);
338 passed &= thread->getRobustList() == 0 && fixture->held.owner == (OwnerDied | Waiters) &&
339 fixture->acquiring.owner == OwnerDied &&
340 fixture->foreign.owner == context->foreign->getTaskId();
341 // Keep one valid owned entry registered to check the actual shutdown hook
342 // from the parent after this Thread has completed.
343 fixture->held.owner = static_cast<uint32_t>(thread->getTaskId());
344 fixture->held.next = address;
345 fixture->pending = 0;
346 context->expectedExitOwner = OwnerDied;
347 passed &= posix_set_robust_list(reinterpret_cast<robust_list_head*>(address),
348 3 * sizeof(uintptr_t), true) == 0;
349 context->passed = passed;
350 return passed ? 0 : 1;
351}
352
353int cleanupWorker(void* parameter) {
354 ContractContext* context = reinterpret_cast<ContractContext*>(parameter);
355 const size_t pageSize = PhysicalMemoryManager::getPageSize();
356 if (context->address) {
357 MemoryMapManager::instance().remove(context->address, pageSize);
358 Processor::information().getCurrentThread()->getParent()->freeUserRange(
359 Process::UserRegion::Normal, context->address, pageSize);
360 }
361 return 0;
362}
363} // namespace
364
365bool runHostedFutexRobustRegressions(Process* kernelProcess) {
366 bool passed = crossOwnerRequeue();
367 PosixProcess* process = new PosixProcess(kernelProcess);
368 process->setSubsystem(new PosixSubsystem);
369 process->publish();
370 PosixProcess* foreign = new PosixProcess(kernelProcess);
371 foreign->setSubsystem(new PosixSubsystem);
372 foreign->publish();
373 PeerContext foreignContext;
374 Thread* foreignThread =
375 new Thread(foreign, peerWorker, &foreignContext, nullptr, false, true, true);
376 const bool foreignStarted = foreignThread->start();
377 passed &= foreignStarted && awaitReady(foreignContext.ready);
378 ContractContext context{foreignThread};
379 Thread* worker = new Thread(process, contractWorker, &context, nullptr, false, true, true);
380 const bool started = worker->start();
381 const bool joined = started && worker->joinForCompletion();
382 if (!started) {
383 delete worker;
384 }
385 uint32_t exitOwner = 0;
386 const bool recovered =
387 context.address &&
388 process->getAddressSpace()->tryReadUser32(
389 context.address + offsetof(UserFixture, held) + offsetof(RobustNode, owner), exitOwner) &&
390 exitOwner == context.expectedExitOwner;
391 passed &= joined && context.passed && recovered;
392 releasePeer(foreignContext);
393 const bool foreignJoined = foreignStarted && foreignThread->joinForCompletion();
394 if (!foreignStarted) {
395 delete foreignThread;
396 }
397 passed &= foreignJoined;
398 Thread* cleanup = new Thread(process, cleanupWorker, &context, nullptr, false, true);
399 passed &= cleanup->joinForCompletion();
400 delete foreign;
401 delete process;
402 if (passed) {
403 NOTICE("HOSTED-SYSCALL-TEST: PASS futex-robust-contracts");
404 } else {
405 ERROR("HOSTED-SYSCALL-TEST: FAIL futex-robust-contracts: queries=" << context.passed
406 << ", exit=" << recovered);
407 }
408 return passed;
409}
Memory-mapped file interface.
MemoryMappedObject * mapAnon(uintptr_t &address, size_t length, MemoryMappedObject::Permissions perms)
size_t remove(uintptr_t base, size_t length)
static MemoryMapManager & instance()
static bool copyToUser(void *destination, const void *source, size_t count, size_t elementSize=1)
size_t getId()
Definition Process.h:463
VirtualAddressSpace * getAddressSpace()
Definition Process.h:478
void publish()
Definition Process.cc:832
static ProcessorInformation & information()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
void setErrno(size_t err)
Definition Thread.h:478
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3184
size_t getErrno()
Definition Thread.h:473
bool joinForCompletion()
Definition Thread.cc:2771
Process * getParent() const
Definition Thread.h:338
size_t getId()
Definition Thread.h:463
bool start()
Definition Thread.cc:794
size_t getTaskId() const
Definition Thread.h:468
virtual bool tryCompareExchangeUser32(uintptr_t address, uint32_t &expected, uint32_t desired, bool &exchanged)
virtual bool tryReadUser32(uintptr_t address, uint32_t &value)
size_t wakeAndRequeue(const Channel &source, size_t wakeCount, const Channel &destination, size_t requeueCount)
Definition WaitQueue.cc:182