The Pedigree Project 0.1
network-stack-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/Scheduler.h"
11#include "pedigree/kernel/process/Semaphore.h"
12#include "pedigree/kernel/process/Thread.h"
13#include "pedigree/kernel/processor/Processor.h"
14#include "pedigree/kernel/time/Time.h"
15#include "pedigree/kernel/utilities/new"
16
20#include "modules/system/network-stack/NetworkStack.h"
21#include "system/kernel/core/processor/DeviceHardIrqContext.h"
22
23namespace {
24class HostedNetworkDevice final : public Network {
25 public:
26 bool send(size_t, uintptr_t) override {
27 return true;
28 }
29
30 const StationInfo& getStationInfo() override {
31 return m_StationInfo;
32 }
33};
34
35struct ReceivePublicationContext {
36 static constexpr size_t MaxTrackedBuffers = 257;
37
38 ReceivePublicationContext(Network* device, bool holdFirst)
39 : device(device),
40 holdFirst(holdFirst),
41 workerEntered(0),
42 allowDispatch(0),
43 queued(0),
44 queuedWithInterruptsDisabled(0),
45 beforeDispatch(0),
46 delivered(0),
47 staleDiscards(0),
48 cancellations(0),
49 failures(0) {}
50
51 Network* device;
52 bool holdFirst;
53 Semaphore workerEntered;
54 Semaphore allowDispatch;
55 Atomic<size_t> queued;
56 Atomic<size_t> queuedWithInterruptsDisabled;
57 Atomic<size_t> beforeDispatch;
58 Atomic<size_t> delivered;
59 Atomic<size_t> staleDiscards;
60 Atomic<size_t> cancellations;
61 Atomic<size_t> failures;
62 uintptr_t buffers[MaxTrackedBuffers] = {};
63 uint8_t terminalCounts[MaxTrackedBuffers] = {};
64
65 bool everyBufferTerminatedOnce(size_t expected) const {
66 if (expected > MaxTrackedBuffers) {
67 return false;
68 }
69 for (size_t i = 0; i < expected; ++i) {
70 if (!buffers[i] || terminalCounts[i] != 1) {
71 return false;
72 }
73 }
74 return true;
75 }
76};
77
78ReceivePublicationContext* g_ReceivePublicationContext = nullptr;
79
80void receivePublicationHook(NetworkStack::HostedReceiveEvent event, uintptr_t buffer,
81 Network* device, size_t) {
82 ReceivePublicationContext* context = g_ReceivePublicationContext;
83 if (!context || device != context->device) {
84 return;
85 }
86
87 bool terminalEvent = false;
88 switch (event) {
89 case NetworkStack::HostedReceiveEvent::Queued: {
90 const size_t index = (context->queued += 1) - 1;
91 if (index >= ReceivePublicationContext::MaxTrackedBuffers) {
92 context->failures += 1;
93 break;
94 }
95 context->buffers[index] = buffer;
97 context->queuedWithInterruptsDisabled += 1;
98 }
99 break;
100 }
101 case NetworkStack::HostedReceiveEvent::BeforeDispatch:
102 if ((context->beforeDispatch += 1) == 1 && context->holdFirst) {
103 context->workerEntered.release();
104 if (!context->allowDispatch.acquireForCompletion()) {
105 context->failures += 1;
106 }
107 }
108 break;
109 case NetworkStack::HostedReceiveEvent::Delivered:
110 context->delivered += 1;
111 terminalEvent = true;
112 break;
113 case NetworkStack::HostedReceiveEvent::DiscardedStale:
114 context->staleDiscards += 1;
115 terminalEvent = true;
116 break;
117 case NetworkStack::HostedReceiveEvent::Cancelled:
118 context->cancellations += 1;
119 terminalEvent = true;
120 break;
121 }
122
123 if (!terminalEvent) {
124 return;
125 }
126
127 bool found = false;
128 for (size_t i = 0; i < context->queued; ++i) {
129 if (context->buffers[i] != buffer) {
130 continue;
131 }
132 found = true;
133 if (++context->terminalCounts[i] != 1) {
134 context->failures += 1;
135 }
136 break;
137 }
138 if (!found) {
139 context->failures += 1;
140 }
141}
142
143struct ReceiveAbaContext {
144 explicit ReceiveAbaContext(Network* device)
145 : device(device),
146 workerEntered(0),
147 allowDispatch(0),
148 buffer(0),
149 generation(0),
150 queued(0),
151 beforeDispatch(0),
152 delivered(0),
153 staleDiscards(0),
154 cancellations(0),
155 failures(0) {}
156
157 Network* device;
158 Semaphore workerEntered;
159 Semaphore allowDispatch;
160 Atomic<uintptr_t> buffer;
161 Atomic<size_t> generation;
162 Atomic<size_t> queued;
163 Atomic<size_t> beforeDispatch;
164 Atomic<size_t> delivered;
165 Atomic<size_t> staleDiscards;
166 Atomic<size_t> cancellations;
167 Atomic<size_t> failures;
168};
169
170ReceiveAbaContext* g_ReceiveAbaContext = nullptr;
171
172void receiveHook(NetworkStack::HostedReceiveEvent event, uintptr_t buffer, Network* device,
173 size_t generation) {
174 ReceiveAbaContext* context = g_ReceiveAbaContext;
175 if (!context || device != context->device) {
176 return;
177 }
178
179 if (event == NetworkStack::HostedReceiveEvent::Queued) {
180 context->queued += 1;
181 if (!context->buffer.compareAndSwap(0, buffer)) {
182 context->failures += 1;
183 }
184 context->generation = generation;
185 return;
186 }
187
188 if (buffer != static_cast<uintptr_t>(context->buffer)) {
189 return;
190 }
191
192 switch (event) {
193 case NetworkStack::HostedReceiveEvent::BeforeDispatch:
194 if ((context->beforeDispatch += 1) != 1) {
195 context->failures += 1;
196 return;
197 }
198 context->workerEntered.release();
199 if (!context->allowDispatch.acquireForCompletion()) {
200 context->failures += 1;
201 }
202 break;
203 case NetworkStack::HostedReceiveEvent::Delivered:
204 context->delivered += 1;
205 break;
206 case NetworkStack::HostedReceiveEvent::DiscardedStale:
207 context->staleDiscards += 1;
208 break;
209 case NetworkStack::HostedReceiveEvent::Cancelled:
210 context->cancellations += 1;
211 break;
212 case NetworkStack::HostedReceiveEvent::Queued:
213 break;
214 }
215}
216
217bool check(bool condition, const char* detail) {
218 if (condition) {
219 return true;
220 }
221
222 ERROR("HOSTED-NETWORK-TEST: FAIL receive-generation-aba: " << detail);
223 return false;
224}
225
226bool check(bool condition, const char* test, const char* detail) {
227 if (condition) {
228 return true;
229 }
230
231 ERROR("HOSTED-NETWORK-TEST: FAIL " << test << ": " << detail);
232 return false;
233}
234
235bool isRegistered(NetworkStack& stack, Network* device) {
237 return stack.acquireDevice(device, lease);
238}
239
240bool waitUntilQueued(Thread* thread, size_t debugState) {
241 const Time::Timestamp deadline = Time::getTicks() + (500 * Time::Multiplier::Millisecond);
242 while (Time::getTicks() < deadline) {
243 Thread::WaitDebugInfo info = {};
244 uintptr_t debugAddress = 0;
245 if (thread->getWaitDebugInfo(info) && info.queue && info.queued &&
246 thread->getDebugState(debugAddress) == debugState) {
247 return true;
248 }
250 }
251 return false;
252}
253
254struct DeviceDeregisterContext {
255 DeviceDeregisterContext(NetworkStack* stack, Network* device)
256 : stack(stack), device(device), entered(0), returned(0) {}
257
258 NetworkStack* stack;
259 Network* device;
260 Atomic<size_t> entered;
261 Atomic<size_t> returned;
262};
263
264int deregisterLeasedDevice(void* parameter) {
265 DeviceDeregisterContext* context = reinterpret_cast<DeviceDeregisterContext*>(parameter);
266 context->entered += 1;
267 context->stack->deRegisterDevice(context->device);
268 context->returned += 1;
269 return 0;
270}
271
272bool deviceLeaseDeregisterDrain() {
273 static const char* Test = "device-lease-deregister-drain";
274
276 alignas(HostedNetworkDevice) uint8_t deviceStorage[sizeof(HostedNetworkDevice)];
277 HostedNetworkDevice* original = new (deviceStorage) HostedNetworkDevice();
278 stack.registerDevice(original);
279 const size_t originalGeneration = NetworkStack::getHostedRegistrationGeneration(original);
280
282 const bool acquired = stack.acquireDevice(original, held);
283 struct netif* originalInterface = acquired ? held.interface() : nullptr;
284 DeviceDeregisterContext context(&stack, original);
285 Thread* remover = nullptr;
286 bool drainPublished = false;
287 bool lateRejected = false;
288 bool usableWhileDraining = false;
289
290 if (acquired) {
291 remover = new Thread(Scheduler::instance().getKernelProcess(), deregisterLeasedDevice, &context,
292 nullptr, false, true);
293 remover->setName("hosted network device deregister");
294 drainPublished = waitUntilQueued(remover, Thread::CallbackDrain);
295
297 lateRejected = !stack.acquireDevice(original, late);
298
299 String name;
300 held.device()->getName(name);
301 const StationInfo& info = held.device()->getStationInfo();
302 usableWhileDraining = held.device() == original && held.interface() == originalInterface &&
303 originalInterface && originalInterface->state == original &&
304 info.nPackets == 0;
306 }
307
308 const bool removerJoined = remover && remover->join();
309 const bool unregistered =
310 !isRegistered(stack, original) && !NetworkStack::getHostedRegistrationGeneration(original);
311 if (!remover) {
312 stack.deRegisterDevice(original);
313 }
314 original->~HostedNetworkDevice();
315
316 HostedNetworkDevice* replacement = new (deviceStorage) HostedNetworkDevice();
317 stack.registerDevice(replacement);
318 const size_t replacementGeneration = NetworkStack::getHostedRegistrationGeneration(replacement);
319 NetworkStack::DeviceLease replacementLease;
320 const bool replacementRegistered = stack.acquireDevice(replacement, replacementLease);
321 replacementLease = NetworkStack::DeviceLease();
322 stack.deRegisterDevice(replacement);
323 replacement->~HostedNetworkDevice();
324
325 const bool passed =
326 check(acquired && originalGeneration && context.entered == 1 && drainPublished &&
327 context.returned == 1 && lateRejected && usableWhileDraining && removerJoined &&
328 unregistered && replacementRegistered && replacementGeneration &&
329 replacementGeneration != originalGeneration,
330 Test, "deregistration did not unpublish, drain, and retire one held registration");
331 if (passed) {
332 NOTICE("HOSTED-NETWORK-TEST: PASS " << Test);
333 }
334 return passed;
335}
336
337bool mailboxTryPostRejectsHardIrq() {
338 static const char* Test = "mailbox-hard-irq-trypost";
339
340 sys_mbox_t mailbox = nullptr;
341 if (sys_mbox_new(&mailbox, 1) != ERR_OK) {
342 return check(false, Test, "mailbox creation failed");
343 }
344
345 int payload = 0;
346 err_t result = ERR_OK;
347 size_t previousDepth = 0;
348 bool restorationArmed = false;
349 {
350 DeviceHardIrqContext hardIrq(previousDepth, restorationArmed);
351 result = sys_mbox_trypost(&mailbox, &payload);
352 }
353
354 void* fetched = nullptr;
355 const u32_t fetchResult = sys_arch_mbox_tryfetch(&mailbox, &fetched);
356 sys_mbox_free(&mailbox);
357
358 const bool passed =
359 check(result == ERR_WOULDBLOCK && fetchResult == SYS_MBOX_EMPTY && !fetched, Test,
360 "the ISR call enqueued data or entered a blocking notification path");
361 if (passed) {
362 NOTICE("HOSTED-NETWORK-TEST: PASS " << Test);
363 }
364 return passed;
365}
366
367bool interruptReceivePublication() {
368 static const char* Test = "receive-interrupt-publication";
369
371 HostedNetworkDevice device;
372 stack.registerDevice(&device);
373
374 ReceivePublicationContext context(&device, true);
375 g_ReceivePublicationContext = &context;
376 NetworkStack::setHostedReceiveHook(receivePublicationHook);
377
378 uint8_t packet[64] = {};
379 const bool interrupts = Processor::getInterrupts();
381 stack.receive(sizeof(packet), reinterpret_cast<uintptr_t>(packet), &device, 0);
382 const bool remainedDisabled = !Processor::getInterrupts();
383 Processor::setInterrupts(interrupts);
384
385 const bool workerHeld = context.workerEntered.acquire(1, 2);
386 bool unregistered = false;
387 if (workerHeld) {
388 stack.deRegisterDevice(&device);
389 unregistered =
390 !isRegistered(stack, &device) && !NetworkStack::getHostedRegistrationGeneration(&device);
391 }
392 context.allowDispatch.release();
393 const bool drained = stack.drain();
394 NetworkStack::setHostedReceiveHook(nullptr);
395 g_ReceivePublicationContext = nullptr;
396 if (!unregistered) {
397 stack.deRegisterDevice(&device);
398 }
399
400 const size_t terminalOwnershipEvents = static_cast<size_t>(context.delivered) +
401 static_cast<size_t>(context.staleDiscards) +
402 static_cast<size_t>(context.cancellations);
403
404 bool passed = true;
405 passed &= check(interrupts && remainedDisabled && context.queued == 1 &&
406 context.queuedWithInterruptsDisabled == 1,
407 Test, "receive did not publish while preserving IF=0");
408 passed &= check(workerHeld && unregistered && drained && context.beforeDispatch == 1 &&
409 !context.delivered && context.staleDiscards == 1 && !context.cancellations &&
410 terminalOwnershipEvents == 1 && context.everyBufferTerminatedOnce(1) &&
411 !context.failures,
412 Test, "the interrupt-published pbuf did not transfer exactly once");
413 passed &= check(NetworkStack::getHostedReceiveRequestCapacity() == 256, Test,
414 "the preallocated bank no longer preserves receive capacity");
415
416 if (passed) {
417 NOTICE("HOSTED-NETWORK-TEST: PASS " << Test);
418 }
419 return passed;
420}
421
422bool boundedReceiveBurst() {
423 static const char* Test = "receive-bounded-burst";
424
426 HostedNetworkDevice device;
427 stack.registerDevice(&device);
428
429 ReceivePublicationContext context(&device, true);
430 g_ReceivePublicationContext = &context;
431 NetworkStack::setHostedReceiveHook(receivePublicationHook);
432
433 uint8_t packet[64] = {};
434 stack.receive(sizeof(packet), reinterpret_cast<uintptr_t>(packet), &device, 0);
435 const bool workerHeld = context.workerEntered.acquire(1, 2);
436 const size_t capacity = NetworkStack::getHostedReceiveRequestCapacity();
437 if (workerHeld) {
438 // The active request owns one token. Fill every remaining token, then
439 // submit one packet whose ownership must be cancelled synchronously.
440 for (size_t i = 1; i <= capacity; ++i) {
441 stack.receive(sizeof(packet), reinterpret_cast<uintptr_t>(packet), &device, 0);
442 }
443 }
444 const bool rejectedWhileHeld = context.cancellations == 1;
445
446 bool unregistered = false;
447 if (workerHeld) {
448 stack.deRegisterDevice(&device);
449 unregistered =
450 !isRegistered(stack, &device) && !NetworkStack::getHostedRegistrationGeneration(&device);
451 }
452
453 // Also releases a worker which arrived after the bounded wait above.
454 context.allowDispatch.release();
455 const bool drained = stack.drain();
456
457 NetworkStack::setHostedReceiveHook(nullptr);
458 g_ReceivePublicationContext = nullptr;
459 if (!unregistered) {
460 stack.deRegisterDevice(&device);
461 }
462
463 const size_t terminalOwnershipEvents = static_cast<size_t>(context.delivered) +
464 static_cast<size_t>(context.staleDiscards) +
465 static_cast<size_t>(context.cancellations);
466
467 bool passed = true;
468 passed &= check(workerHeld && rejectedWhileHeld && unregistered && !context.failures, Test,
469 "the full token bank did not reject one pbuf synchronously");
470 passed &= check(drained && context.queued == capacity + 1 && context.beforeDispatch == capacity &&
471 !context.delivered && context.staleDiscards == capacity &&
472 context.cancellations == 1,
473 Test, "the accepted/rejected burst counts were inconsistent");
474 passed &= check(
475 terminalOwnershipEvents == capacity + 1 && context.everyBufferTerminatedOnce(capacity + 1),
476 Test, "a burst pbuf did not have exactly one terminal owner");
477
478 if (passed) {
479 NOTICE("HOSTED-NETWORK-TEST: PASS " << Test);
480 }
481 return passed;
482}
483
484bool queuedReceiveGenerationAba() {
486 alignas(HostedNetworkDevice) uint8_t deviceStorage[sizeof(HostedNetworkDevice)];
487 HostedNetworkDevice* original = new (deviceStorage) HostedNetworkDevice();
488
489 stack.registerDevice(original);
490 const size_t originalGeneration = NetworkStack::getHostedRegistrationGeneration(original);
491 bool passed = true;
492 passed &= check(isRegistered(stack, original) && originalGeneration,
493 "the original device was not registered");
494
495 ReceiveAbaContext context(original);
496 g_ReceiveAbaContext = &context;
497 NetworkStack::setHostedReceiveHook(receiveHook);
498
499 uint8_t packet[64] = {};
500 stack.receive(sizeof(packet), reinterpret_cast<uintptr_t>(packet), original, 0);
501
502 const bool workerHeld = context.workerEntered.acquire(1, 2);
503 HostedNetworkDevice* replacement = nullptr;
504 size_t replacementGeneration = 0;
505 bool unregistered = false;
506 bool reusedAddress = false;
507 bool replacementRegistered = false;
508
509 if (workerHeld) {
510 stack.deRegisterDevice(original);
511 unregistered =
512 !isRegistered(stack, original) && !NetworkStack::getHostedRegistrationGeneration(original);
513 original->~HostedNetworkDevice();
514
515 replacement = new (deviceStorage) HostedNetworkDevice();
516 reusedAddress = replacement == original;
517 stack.registerDevice(replacement);
518 replacementGeneration = NetworkStack::getHostedRegistrationGeneration(replacement);
519 replacementRegistered = isRegistered(stack, replacement) && replacementGeneration;
520 }
521
522 // Also makes a late-arriving worker safe after a timeout above.
523 context.allowDispatch.release();
524 const bool drained = stack.drain();
525
526 NetworkStack::setHostedReceiveHook(nullptr);
527 g_ReceiveAbaContext = nullptr;
528
529 if (replacement) {
530 stack.deRegisterDevice(replacement);
531 replacement->~HostedNetworkDevice();
532 } else {
533 stack.deRegisterDevice(original);
534 original->~HostedNetworkDevice();
535 }
536
537 const size_t terminalOwnershipEvents = static_cast<size_t>(context.delivered) +
538 static_cast<size_t>(context.staleDiscards) +
539 static_cast<size_t>(context.cancellations);
540
541 passed &= check(workerHeld && drained && !context.failures,
542 "the queued receive could not be held and drained deterministically");
543 passed &= check(context.queued == 1 && context.beforeDispatch == 1 &&
544 context.generation == originalGeneration,
545 "the queued request did not retain the original registration");
546 passed &= check(unregistered && reusedAddress && replacementRegistered &&
547 replacementGeneration != originalGeneration,
548 "unregister/re-register did not create a new identity at one address");
549 passed &= check(context.staleDiscards == 1 && !context.delivered && !context.cancellations,
550 "stale queued work reached the replacement or the wrong release path");
551 passed &= check(terminalOwnershipEvents == 1,
552 "the receive buffer did not have exactly one terminal owner");
553
554 if (passed) {
555 NOTICE("HOSTED-NETWORK-TEST: PASS receive-generation-aba");
556 }
557 return passed;
558}
559} // namespace
560
561bool runHostedNetworkStackRegressions() {
562 bool passed = mailboxTryPostRejectsHardIrq();
563 passed &= deviceLeaseDeregisterDrain();
564 passed &= interruptReceivePublication();
565 passed &= boundedReceiveBurst();
566 passed &= queuedReceiveGenerationAba();
567 return passed;
568}
void deRegisterDevice(Network *pDevice)
static NetworkStack & instance()
void receive(size_t nBytes, uintptr_t packet, Network *pCard, uint32_t offset)
MUST_USE_RESULT bool acquireDevice(size_t n, DeviceLease &lease)
void registerDevice(Network *pDevice)
virtual const StationInfo & getStationInfo()
Definition Network.cc:79
virtual bool send(size_t nBytes, uintptr_t buffer)=0
virtual void getName(String &str)
Definition Network.cc:67
static bool getInterrupts()
static void setInterrupts(bool bEnable)
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3184
bool join()
Definition Thread.cc:2767
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:570
s8_t err_t
Definition err.h:76
@ ERR_OK
Definition err.h:82
@ ERR_WOULDBLOCK
Definition err.h:96
err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
Definition sys_arch.cc:321
err_t sys_mbox_new(sys_mbox_t *mbox, int size)
Definition sys_arch.cc:250
void sys_mbox_free(sys_mbox_t *mbox)
Definition sys_arch.cc:255
u32_t sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
Definition sys_arch.cc:279
Definition netif.h:244
char name[2]
Definition netif.h:326
void * state
Definition netif.h:298
#define SYS_MBOX_EMPTY
Definition sys.h:111