CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
ProcessMemoryFuzz.cpp
Go to the documentation of this file.
1
26
27#include "ProcessMemory.h"
28#include "MemoryManager.h"
29#include "UnitTestFramework.h"
30#include <deque>
31#include <stdlib.h>
32#include <string.h>
33
34namespace cmlabs {
35
37struct RingRand {
38 uint64 s;
39 RingRand(uint64 seed) : s(seed ? seed : 0x9E3779B97F4A7C15ULL) {}
40 uint64 next() { s ^= s >> 12; s ^= s << 25; s ^= s >> 27; return s * 2685821657736338717ULL; }
41 uint32 below(uint32 n) { return n ? (uint32)(next() % n) : 0; }
42 bool chance(uint32 pct) { return below(100) < pct; }
43};
44
46struct RingRef {
47 uint32 serial;
48 uint32 payloadLen;
49 uint32 sum;
50 uint32 wireSize;
51};
52
54static uint32 RingChecksum(const char* p, uint32 len) {
55 uint32 h = 2166136261u;
56 for (uint32 i = 0; i < len; i++) { h ^= (uint8)p[i]; h *= 16777619u; }
57 return h;
58}
59
62static void RingFillPayload(char* buf, uint32 len, uint32 serial) {
63 for (uint32 i = 0; i < len; i++)
64 buf[i] = (char)('A' + ((serial * 31 + i * 7) % 26));
65 buf[len] = 0;
66}
67
69static uint32 RingEnvU32(const char* name, uint32 def) {
70 const char* v = getenv(name);
71 if (!v || !*v) return def;
72 long n = atol(v);
73 return (n > 0) ? (uint32)n : def;
74}
75
76
78static const char* RingClass(const RingRef& want, DataMessage* got, uint32& gotSerial,
79 uint32& gotLen, uint32& gotSum) {
80 if (!got) return "LOST"; // model had an entry, queue gave nothing
81 uint32 sz = got->getSize();
82 if (sz != want.wireSize) return "SHORT";
83 bool ok = false;
84 int64 s = got->getInt("s", ok); if (!ok) return "CORRUPT";
85 gotSerial = (uint32)s;
86 uint32 plen = 0;
87 const char* body = got->getData("p", plen);
88 gotLen = plen;
89 gotSum = body ? RingChecksum(body, plen) : 0;
90 if (gotSerial != want.serial) return "REORDERED";
91 if (gotLen != want.payloadLen) return "SHORT";
92 if (gotSum != want.sum) return "CORRUPT";
93 return NULL; // match
94}
95
98static DataMessage* RingMakeMsg(uint32 serial, uint32 len, RingRef& ref) {
100 m->setInt("s", serial);
101 char* buf = new char[len + 1];
102 RingFillPayload(buf, len, serial);
103 m->setData("p", buf, len);
104 delete [] buf;
105 ref.serial = serial;
106 ref.payloadLen = len;
107 ref.sum = RingChecksum(NULL, 0); // placeholder, recomputed below
108 // recompute sum from the same bytes we just wrote
109 {
110 char* tmp = new char[len + 1];
111 RingFillPayload(tmp, len, serial);
112 ref.sum = RingChecksum(tmp, len);
113 delete [] tmp;
114 }
115 ref.wireSize = m->getSize();
116 return m;
117}
118
131static const char* RingCheckInvariants(MessageQueueHeader* q, uint32 expectCount,
132 char* detail, uint32 detailSize) {
133 if (!q) { return expectCount == 0 ? NULL : "HEADER_NULL"; }
134
135 uint64 cap = q->size - sizeof(MessageQueueHeader); // circular-buffer capacity
136 char* data = ((char*)q) + sizeof(MessageQueueHeader);
137
138 // 1) count must match what the model expects (the primary handover symptom).
139 if (q->count != expectCount) {
140 snprintf(detail, detailSize,
141 "count=%u expect=%u startPos=%llu endPos=%llu padding=%llu cap=%llu",
142 q->count, expectCount, q->startPos, q->endPos, q->padding, cap);
143 return "COUNT_MISMATCH";
144 }
145
146 // 2) positions must be inside the buffer.
147 if (q->startPos > cap || q->endPos > cap || q->padding > cap) {
148 snprintf(detail, detailSize,
149 "startPos=%llu endPos=%llu padding=%llu cap=%llu",
150 q->startPos, q->endPos, q->padding, cap);
151 return "POS_OUT_OF_RANGE";
152 }
153
154 // 3) empty ambiguity: count==0 but startPos!=endPos means the ring thinks it is empty
155 // while its pointers still span live bytes (exactly the "reader stops early" shape).
156 if (q->count == 0 && q->startPos != q->endPos) {
157 snprintf(detail, detailSize, "count=0 but startPos=%llu endPos=%llu padding=%llu",
158 q->startPos, q->endPos, q->padding);
159 return "EMPTY_BUT_SPANNED";
160 }
161
162 // 4) walk exactly count messages from startPos, honouring the same wrap rule the reader
163 // uses (rewind when we reach cap - padding), and prove each is a well-formed DataMessage
164 // of non-zero size that fits before the wrap point.
165 // NB the reader rewinds *before* consuming the next message, not after the last one:
166 // a queue filled exactly to the end legally has endPos == cap (addToQ branch 2 does
167 // endPos += msgSize without wrapping), so rewinding after the final message would
168 // wrongly collapse cap -> 0 and flag a phantom mismatch. Rewind only when we still have
169 // another message to place.
170 uint64 pos = q->startPos;
171 for (uint32 i = 0; i < q->count; i++) {
172 // rewind before reading, matching the reader's own timing
173 if (pos >= cap - q->padding) pos = 0;
174 if (GetObjID(data + pos) != DATAMESSAGEID) {
175 snprintf(detail, detailSize,
176 "msg %u/%u at pos=%llu not a DataMessage (startPos=%llu endPos=%llu padding=%llu)",
177 i, q->count, pos, q->startPos, q->endPos, q->padding);
178 return "WALK_BAD_OBJID";
179 }
180 uint32 msz = ((DataMessageHeader*)(data + pos))->size;
181 if (msz == 0 || pos + msz > cap) {
182 snprintf(detail, detailSize,
183 "msg %u/%u pos=%llu size=%u overruns cap=%llu (padding=%llu)",
184 i, q->count, pos, msz, cap, q->padding);
185 return "WALK_SIZE_OVERRUN";
186 }
187 pos += msz;
188 }
189
190 // 5) after walking count messages we must land exactly on endPos (single-process: no
191 // concurrent writer moved it). A mismatch is the count-vs-contents inconsistency.
192 // endPos == cap is legal (full-to-the-end, not yet wrapped); so is endPos in [0,cap].
193 if (pos != q->endPos) {
194 snprintf(detail, detailSize,
195 "walk ended at pos=%llu but endPos=%llu (count=%u startPos=%llu padding=%llu cap=%llu)",
196 pos, q->endPos, q->count, q->startPos, q->padding, cap);
197 return "WALK_ENDPOS_MISMATCH";
198 }
199 return NULL;
200}
201
204static bool RingRunSeed(MemoryManager* manager, uint64 seed, uint32 iters, bool selftest,
205 bool checkInvariants, bool invariantSelftest) {
206 RingRand rng(seed);
207 std::deque<RingRef> model;
208 uint32 nextSerial = 1;
209 uint32 injectAt = selftest ? (iters / 2) : 0xFFFFFFFFu;
210 bool injected = false; // positive control: fire exactly once, on a read
211 char idet[256];
212
213 // Invariant positive control: once, after a write, deliberately corrupt the live
214 // header's count so RingCheckInvariants MUST catch it (proves the checker can fail).
215 uint32 invInjectAt = invariantSelftest ? (iters / 2) : 0xFFFFFFFFu;
216 bool invInjected = false;
217
218 // Resize-heavy mode (PSY_RING_RESIZEHEAVY=1): bias hard toward large writes and deep
219 // queues so addToQ keeps hitting the resize path and waitForQ keeps reading across a
220 // buffer that just doubled with stale padding -- the exact arithmetic the handover flags
221 // as never cleanly exonerated. Off by default so the validated default mix is unchanged.
222 bool resizeHeavy = getenv("PSY_RING_RESIZEHEAVY") && *getenv("PSY_RING_RESIZEHEAVY") == '1';
223
224 for (uint32 op = 0; op < iters; op++) {
225 // Bias toward writes when near-empty and reads when large, to sweep both wrap
226 // directions and force resize, drain-to-empty and exact-fit states.
227 uint32 depth = (uint32)model.size();
228 bool doWrite;
229 // Force a read once we reach injectAt with something queued, so the positive
230 // control is guaranteed to exercise the oracle rather than depending on the RNG.
231 bool forceRead = (op >= injectAt) && !injected && depth > 0;
232 if (forceRead) doWrite = false;
233 else if (depth == 0) doWrite = true;
234 else if (resizeHeavy) doWrite = (depth > 400) ? rng.chance(45) : rng.chance(80);
235 else if (depth > 200) doWrite = rng.chance(20);
236 else doWrite = rng.chance(60);
237
238 if (doWrite) {
239 // occasional exact-fit-ish lengths plus a broad spread up to ~1.5KB
240 uint32 len;
241 uint32 pick = rng.below(100);
242 if (resizeHeavy) {
243 // mostly large payloads to drive repeated buffer doublings, plus some tiny
244 // ones so wraps leave odd startPos/padding remainders behind
245 if (pick < 20) len = 1 + rng.below(16);
246 else len = 700 + rng.below(1400);
247 }
248 else if (pick < 15) len = 1 + rng.below(8); // tiny
249 else if (pick < 25) len = 900 + rng.below(700); // large: forces resize/wrap
250 else len = 1 + rng.below(300); // medium
251 RingRef ref;
252 DataMessage* m = RingMakeMsg(nextSerial++, len, ref);
253 if (!manager->processMemory->addToMsgQ(0, m)) {
254 unittest::fail("seed %llu op %u: addToMsgQ failed (serial %u len %u)",
255 seed, op, ref.serial, len);
256 delete m;
257 return false;
258 }
259 delete m;
260 model.push_back(ref);
261
262 if (checkInvariants) {
264 if ((op >= invInjectAt) && !invInjected && q) { q->count += 7; invInjected = true; }
265 const char* iv = RingCheckInvariants(q, (uint32)model.size(), idet, sizeof(idet));
266 if (iv) {
267 unittest::fail("seed %llu op %u (post-write serial %u): INVARIANT %s :: %s",
268 seed, op, ref.serial, iv, idet);
269 return false;
270 }
271 }
272 }
273 else {
274 RingRef want = model.front();
275 model.pop_front();
276 DataMessage* got = manager->processMemory->waitForMsgQ(0, 200);
277
278 // positive control: corrupt the expectation once so a correct queue "diverges",
279 // proving the oracle can actually fail (not just always pass).
280 if ((op >= injectAt) && !injected) { want.serial ^= 0xABCD; injected = true; }
281
282 uint32 gs = 0, gl = 0, gsum = 0;
283 const char* cls = RingClass(want, got, gs, gl, gsum);
284 if (cls) {
285 unittest::fail("seed %llu op %u: %s want{serial=%u len=%u sum=%08x size=%u} "
286 "got{serial=%u len=%u sum=%08x size=%u}",
287 seed, op, cls, want.serial, want.payloadLen, want.sum, want.wireSize,
288 gs, gl, gsum, got ? got->getSize() : 0);
289 if (got) delete got;
290 return false;
291 }
292 delete got;
293
294 if (checkInvariants) {
296 const char* iv = RingCheckInvariants(q, (uint32)model.size(), idet, sizeof(idet));
297 if (iv) {
298 unittest::fail("seed %llu op %u (post-read): INVARIANT %s :: %s",
299 seed, op, iv, idet);
300 return false;
301 }
302 }
303 }
304 }
305
306 while (!model.empty()) {
307 RingRef want = model.front();
308 model.pop_front();
309 DataMessage* got = manager->processMemory->waitForMsgQ(0, 200);
310 uint32 gs = 0, gl = 0, gsum = 0;
311 const char* cls = RingClass(want, got, gs, gl, gsum);
312 if (cls) {
313 unittest::fail("seed %llu drain: %s serial want=%u got=%u", seed, cls, want.serial, gs);
314 if (got) delete got;
315 return false;
316 }
317 delete got;
318 }
319 return true;
320}
321
323 uint32 iters = RingEnvU32("PSY_RING_ITERS", 20000);
324 uint32 seeds = RingEnvU32("PSY_RING_SEEDS", 8);
325 bool selftest = getenv("PSY_RING_SELFTEST") && *getenv("PSY_RING_SELFTEST") == '1';
326 bool checkInvariants = getenv("PSY_RING_INVARIANTS") && *getenv("PSY_RING_INVARIANTS") == '1';
327 bool invariantSelftest = getenv("PSY_RING_INVSELFTEST") && *getenv("PSY_RING_INVSELFTEST") == '1';
328 if (invariantSelftest) checkInvariants = true; // inv-selftest implies the checker is on
329 const char* sEnv = getenv("PSY_RING_SEED");
330
331 // Each seed runs on its OWN freshly created+destroyed MemoryManager segment. A single
332 // shared manager leaks ring geometry between seeds, which (a) makes a failure depend on
333 // the seeds that ran before it, so PSY_RING_SEED replay does NOT reproduce it, and
334 // (b) is exactly the kind of harness artifact the ring-defect handover warns about.
335 bool ok = true;
336 if (sEnv && *sEnv) {
337 uint64 seed = (uint64)strtoull(sEnv, NULL, 0);
338 unittest::progressf(10, "replay seed %llu (%u iters)", seed, iters);
339 MemoryManager* m = new MemoryManager();
340 if (!m->create(0)) { unittest::fail("MemoryManager create(0) failed (replay seed %llu)", seed); delete m; return false; }
341 ok = RingRunSeed(m, seed, iters, selftest, checkInvariants, invariantSelftest);
342 delete m;
343 }
344 else {
345 for (uint32 i = 0; i < seeds && ok; i++) {
346 uint64 seed = 0x1000ULL + i * 0x9E3779B1ULL;
347 unittest::progressf(10 + (int)(80.0 * i / seeds),
348 "seed %llu (%u/%u, %u iters)", seed, i + 1, seeds, iters);
349 MemoryManager* m = new MemoryManager();
350 if (!m->create(0)) { unittest::fail("MemoryManager create(0) failed (seed %llu)", seed); delete m; ok = false; break; }
351 ok = RingRunSeed(m, seed, iters, selftest, checkInvariants, invariantSelftest);
352 delete m;
353 }
354 }
355
356 unittest::progress(100, "done");
357 return ok;
358}
359
360} // namespace cmlabs
Central shared-memory manager for a Psyclone node: master segment, per-subsystem shared maps and the ...
#define DATAMESSAGEID
Definition ObjectIDs.h:75
#define GetObjID(data)
Extract the cid field from a binary object block: the uint32 at byte offset 4 (after the leading size...
Definition ObjectIDs.h:26
Shared-memory process ("space") table plus per-process message queues.
#define MSGQ_TYPE
Data-message queue.
Small, dependency-free unit test harness used by all CMSDK object tests.
The central Psyclone data container: a self-contained binary message with typed, named user entries.
bool getInt(const char *key, int64 &value)
getInt(const char* key, int64& value)
bool setInt(const char *key, int64 value)
setInt(const char* key, int64 value)
const char * getData(const char *key, uint32 &size)
getData(const char* key, uint32& size)
uint32 getSize()
getSize() Get message size Many types of data of any size can be put into a message as user entries; ...
bool setData(const char *key, const char *value, uint32 size)
setData(const char* key, const char* value, uint32 size)
Top-level facade of the shared-memory subsystem for one process.
ProcessMemory * processMemory
Accessor for the process table and per-process queues.
bool create(uint16 sysID, uint32 slotCount=100000, uint16 binCount=2, uint32 minBlockSize=1024, uint32 maxBlockSize=64 *1024, uint64 initSize=50000000L, uint64 maxSize=1000000000L, bool force=false)
Create all shared segments for a new node instance (master process only).
MessageQueueHeader * fuzzPeekQHeader(uint16 procID, uint8 qType)
Diagnostics-only: read-only access to a live queue header for the Phase-2 fuzz invariant checker (Pro...
bool addToMsgQ(uint16 procID, DataMessage *msg)
Enqueue on the data-message queue.
static bool FuzzTest()
Seeded, oracle-checked fuzz of the circular queue arithmetic (single process).
DataMessage * waitForMsgQ(uint16 procID, uint32 timeout)
Wait on the data-message queue.
void fail(const char *fmt,...)
Set an explanatory reason shown on the FAIL line.
void progressf(int percent, const char *fmt,...)
printf-style variant of progress().
void progress(int percent, const char *action)
Report progress with a short description of the current action.
static uint32 RingChecksum(const char *p, uint32 len)
FNV-1a over the payload body.
static uint32 RingEnvU32(const char *name, uint32 def)
Env integer with default.
static DataMessage * RingMakeMsg(uint32 serial, uint32 len, RingRef &ref)
Build one message carrying serial serial and a deterministic payload of len bytes,...
static const char * RingClass(const RingRef &want, DataMessage *got, uint32 &gotSerial, uint32 &gotLen, uint32 &gotSum)
Classifier for a single divergence between the queue and the reference model.
static const char * RingCheckInvariants(MessageQueueHeader *q, uint32 expectCount, char *detail, uint32 detailSize)
Walk the live region of a queue and assert its bookkeeping is self-consistent.
static void RingFillPayload(char *buf, uint32 len, uint32 serial)
Deterministic payload: content is a pure function of (serial, len), so a reader can re-derive what it...
static struct PsyType CTRL_TEST
Definition ObjectIDs.h:83
static bool RingRunSeed(MemoryManager *manager, uint64 seed, uint32 iters, bool selftest, bool checkInvariants, bool invariantSelftest)
Run one seed to completion.
The current (version 10) DataMessage wire/shared-memory header.
On-segment header of a circular message queue (legacy 32-bit layout).
uint32 padding
Alignment/reserved.
uint32 size
Total size of the queue region in bytes (header + buffer).
uint32 count
Number of messages currently in the queue.
uint32 startPos
Read offset into the circular buffer.
uint32 endPos
Write offset into the circular buffer.
Deterministic xorshift64* — same sequence on every platform, unlike rand().
bool chance(uint32 pct)
uint32 below(uint32 n)
One entry of the reference model: what a correct queue MUST hand back, in order.