/* dev_limits.c — the two fixed-size limits in runtime/flan_dev.c, driven * directly. * * Neither is reachable from a Flan program that behaves. The result buffer * only truncates when a renderer emits more than 4K, which no fixture does, * and the name table only fills after 4096 distinct run-time-introduced names, * which is more forms than the whole corpus has. So both were code nothing had * ever run: the first thing either would do in anger is either lose the end of * a value or scribble past the end of a static array, and the ellipsis and the * abort are the two behaviours that say which. * * A C main, for the same reason reload_host.c is one: these are C entry points * with no Flan spelling, and flan_dev.c is compiled into every build (see * build.ml), so linking any Flan program brings them along. The .flan this is * linked against therefore has no [main] of its own. * * One mode per run, chosen by argv, because both of them are one-way: the * name table never shrinks and the overflow case aborts the process. */ #include #include #include #include #include void **flan_dev_cell(const char *name); void flan_dev_result_begin(void); void flan_dev_emit(const uint8_t *bytes, int64_t len); void flan_dev_result_end(void); uint64_t flan_dev_result_cap(void); int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen, uint64_t *len); /* [flan_dev_result_get] was replaced by a real seqlock, which copies into the * caller's buffer rather than handing back a pointer into one being written. * The buffer is sized from [flan_dev_result_cap] rather than from CAP below, * which is this test's own expectation of the answer: sizing it from the * number under test would make a wrong cap copy the wrong amount and agree * with itself. */ static char rbuf[1 << 16]; static const char *result_read(uint64_t *gen, uint64_t *len) { if (flan_dev_result_cap() > sizeof rbuf) { printf("result buffer too small for cap %llu\n", (unsigned long long)flan_dev_result_cap()); exit(2); } if (!flan_dev_result_read(rbuf, sizeof rbuf, gen, len)) { /* Single-threaded here: the writer is this thread, so a failed read means * the counter was left odd, not that a write was racing. */ printf("result read did not settle\n"); exit(2); } return rbuf; } void flan_rt_init(int32_t argc, char **argv); void flan_dev_reg_enable(void); void flan_dev_reg_note(void *base, int64_t bytes, int64_t elem, const char *type, int64_t typelen); void flan_dev_reg_dead(void *base); int flan_dev_reg_overflowed(void); int64_t flan_dev_reg_count(int32_t live_only); int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts, int64_t *bytes, const char **types, int64_t *typelens, int64_t cap, int64_t *unread); #define CAP 4096 /* Emit more than fits, twice the buffer's worth, and report what came back. * What is printed is everything a wrong cap gets wrong: the length, the three * characters that say it was truncated, a byte from the middle to show the * content up to the cut is the content that was emitted, and the generation * counter, which is what a reader waits on and must move exactly once. */ static int cap(void) { static uint8_t a[3000], b[3000]; uint64_t gen0, gen1, len; const char *r; memset(a, 'a', sizeof a); memset(b, 'b', sizeof b); r = result_read(&gen0, &len); flan_dev_result_begin(); flan_dev_emit(a, (int64_t)sizeof a); /* A negative length is not a huge one: the cast to size_t would make it * about 2^64 and the clamp would then be the only thing between it and a * memcpy of everything. */ flan_dev_emit(b, -1); flan_dev_emit(b, (int64_t)sizeof b); flan_dev_result_end(); r = result_read(&gen1, &len); printf("len %llu\n", (unsigned long long)len); printf("tail %.3s\n", len >= 3 ? r + len - 3 : ""); printf("mid %c\n", len > 3500 ? r[3500] : '?'); printf("head %c\n", len > 0 ? r[0] : '?'); printf("gen %llu\n", (unsigned long long)(gen1 - gen0)); /* And a short value after a truncated one: the flag has to be cleared by * [begin] or every later render ends in an ellipsis it did not earn. */ flan_dev_result_begin(); flan_dev_emit((const uint8_t *)"12", 2); flan_dev_result_end(); r = result_read(&gen1, &len); printf("again %.*s\n", (int)len, r); return 0; } /* Fill the name table and then ask for one more. The table is fixed and never * moves — a module holds the address of a cell for as long as it is loaded — * so the only honest answer past the end is to stop. */ static int names(void) { char buf[32]; for (int i = 0; i < CAP; i++) { snprintf(buf, sizeof buf, "n%d", i); if (flan_dev_cell(buf) == NULL) { printf("null cell at %d\n", i); return 1; } } /* Distinct names, all of them: a table that deduplicated wrongly would not * be full here and the line below would not abort. */ printf("interned %d\n", CAP); fflush(stdout); (void)flan_dev_cell("one-too-many"); printf("survived\n"); return 0; } /* ── A table full of live blocks, read while it is being written ───── * * The shape of the bug this pins, which is worth writing down because the * mode looks like a stress test and is not one. A compaction reclaims dead * entries. The trigger used to be "three quarters full" and nothing else, so a * program holding more than three quarters of the table live compacted on * every single allocation, for ever, reclaiming nothing each time. Each * compaction holds the table-wide epoch odd from end to end, a listing retries * eight times and each retry meets another one, and [flan_dev_reg_by_type] * used to answer that with zero rows — "nothing is held", about a program * holding three thousand blocks. It was measured at 198 wrong answers in 200. * * So: 3100 live blocks, which is over the old three-quarter mark and under the * 4096 the table holds, one thread noting allocations as fast as it can, and * this thread asking the leak question two hundred times. The count printed is * how many of those answers were right. Nothing here is timing-dependent in * the direction that matters — a wrong answer is a wrong answer on any * machine, and the fixed table means "right" is a number this file knows. * * The blocks are fake addresses rather than real allocations. The registry is * told about a block, it never reads one, and 3100 real mallocs would only be * measuring the allocator. */ enum { REG_LIVE = 3100, REG_ASKS = 200 }; static volatile int reg_stop; /* The writer: re-note blocks that are already in the table, which is what a * game loop overwriting its own allocations looks like from in here, and is * the path that takes the compaction branch. The addresses are the ones * already recorded, so the live count does not move and the table never * fills. */ static void *reg_writer(void *arg) { int64_t n = 0; (void)arg; while (!reg_stop) { for (int i = 0; i < REG_LIVE && !reg_stop; i++) { void *p = (void *)(uintptr_t)(0x100000 + (uintptr_t)i * 64); flan_dev_reg_note(p, 64, 8, "Enemy", 5); n++; } } return (void *)(uintptr_t)n; } static int regfull(void) { enum { ROWS = 16 }; int64_t counts[ROWS], bytes[ROWS], typelens[ROWS], unread; const char *types[ROWS]; pthread_t w; int right = 0, refused = 0, wrong = 0, zero = 0; flan_dev_reg_enable(); for (int i = 0; i < REG_LIVE; i++) { void *p = (void *)(uintptr_t)(0x100000 + (uintptr_t)i * 64); flan_dev_reg_note(p, 64, 8, "Enemy", 5); } printf("live %lld\n", (long long)flan_dev_reg_count(1)); if (pthread_create(&w, NULL, reg_writer, NULL) != 0) { printf("no writer thread\n"); return 2; } for (int a = 0; a < REG_ASKS; a++) { int64_t n = flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS, &unread); /* One type, 3100 blocks of it. Any other answer is this table described as something it is not — and zero rows is the specific lie, because it is the answer a program that had freed everything would get. */ if (n < 0) refused++; else if (n == 0) zero++; else if (n == 1 && counts[0] == REG_LIVE) right++; else wrong++; } reg_stop = 1; pthread_join(w, NULL); printf("right %d\n", right); printf("zero %d\n", zero); printf("wrong %d\n", wrong); printf("refused %d\n", refused); return 0; } /* ── The dead count the compaction trigger reads ───────────────────── * * [regfull] above proves a table of live blocks stops compacting. This proves * the other half still does, which is the failure that would be silent: the * trigger now asks how many entries are dead, four places maintain that number * — a death, an arena's free-all, a dead slot being written over, and the sweep * itself, which drops all of them — and a count that drifted low would simply * stop reclaiming. Nothing would crash. The table would pin at its cap, the * overflow flag would come on, and every listing from then on would be a floor * quietly described as a count. * * So: three thousand blocks that stay live, and a pool of six hundred noted and * freed over and over on top of them. Used passes the three-quarter mark, the * dead pass the reclaim floor several times over the run, and the sweep runs. * The assertion that catches a drifted count is the last one — a table that * never reclaimed would have overflowed long before this returns. */ enum { CHURN_LIVE = 3000, CHURN_POOL = 600, CHURN_PASSES = 8 }; static void *churn_addr(int i) { return (void *)(uintptr_t)(0x800000 + (uintptr_t)i * 64); } static int regchurn(void) { enum { ROWS = 16 }; int64_t counts[ROWS], bytes[ROWS], typelens[ROWS], unread, n; const char *types[ROWS]; flan_dev_reg_enable(); for (int i = 0; i < CHURN_LIVE; i++) flan_dev_reg_note(churn_addr(i), 64, 8, "Enemy", 5); /* Noted and freed on top of the live set. A note landing on the slot of a * block that died is the decrement; the free is the increment; the sweep in * between is the reset. */ for (int pass = 0; pass < CHURN_PASSES; pass++) for (int i = 0; i < CHURN_POOL; i++) { void *p = churn_addr(CHURN_LIVE + i); flan_dev_reg_note(p, 32, 4, "Bullet", 6); flan_dev_reg_dead(p); } printf("live %lld\n", (long long)flan_dev_reg_count(1)); n = flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS, &unread); printf("rows %lld\n", (long long)n); printf("blocks %lld\n", (long long)(n == 1 ? counts[0] : -1)); /* The one that catches a dead count that drifted: a table that stopped reclaiming fills, and a full table is a floor for the rest of the run. */ printf("overflowed %d\n", flan_dev_reg_overflowed()); return 0; } /* And a table that really is full, which is the case the trigger above must * not paper over: 4096 live blocks and nothing dead anywhere, then one more. * The note is dropped — that is the standing decision, and dying because a * diagnostic ran out of room would be worse — and what is checked here is that * the drop is *said*, once, rather than only being discoverable by asking the * flag. A second and third dropped note must add nothing. */ static int regoverflow(void) { flan_dev_reg_enable(); for (int i = 0; i < CAP; i++) flan_dev_reg_note((void *)(uintptr_t)(0x2000000 + (uintptr_t)i * 64), 64, 8, "Enemy", 5); printf("live %lld\n", (long long)flan_dev_reg_count(1)); printf("overflowed %d\n", flan_dev_reg_overflowed()); fflush(stdout); for (int i = 0; i < 3; i++) flan_dev_reg_note((void *)(uintptr_t)(0x9000000 + (uintptr_t)i * 64), 64, 8, "Latecomer", 9); printf("overflowed %d\n", flan_dev_reg_overflowed()); printf("live %lld\n", (long long)flan_dev_reg_count(1)); return 0; } /* ── A listing taken while the table is actually being rearranged ──── * * [regfull] is a table of live blocks, where the fix is that no compaction * runs at all. This is the other race: a writer churning hard enough that * compactions do run, under a reader asking the leak question through them. * * What it pins is not a rate. Rates here are the machine's, and the counts * printed are deliberately the two that cannot come out any other way on any * machine: [zero], an answer of no rows, which is the original lie and the one * shape a program holding three thousand blocks must never produce; and * [low], a count that is neither right nor a refusal, which is what a torn * read or a walk that straddled a compaction would produce. Right-versus- * refused is the interesting number and it is printed rather than asserted, * because a refusal is an honest answer and how often it comes up is the * writer's business. It moves a lot: on the machine this was written on, the * paced retry turned 8 right in 200 into 200 in 200 at one churn rate and * changed nothing at all at another. * * The throttle is a spin and not a sleep because the whole interesting range * is under nanosleep's floor of tens of microseconds. */ enum { RACE_LIVE = 3000, RACE_ASKS = 200, RACE_SPIN = 1000 }; static volatile int race_stop; static void *race_writer(void *arg) { long long i = 0; (void)arg; while (!race_stop) { /* Noted and freed, which is what makes the dead count climb and the * compaction actually run — the difference from [regfull]'s writer, which * re-notes live blocks and therefore never earns a sweep. */ void *p = (void *)(uintptr_t)(0x40000000 + (uintptr_t)(i % 100000) * 64); flan_dev_reg_note(p, 32, 4, "Bullet", 6); flan_dev_reg_dead(p); for (volatile long k = 0; k < RACE_SPIN; k++) { } i++; } return NULL; } static int regrace(void) { enum { ROWS = 16 }; int64_t counts[ROWS], bytes[ROWS], typelens[ROWS], unread; const char *types[ROWS]; pthread_t w; int right = 0, refused = 0, zero = 0, low = 0; flan_dev_reg_enable(); for (int i = 0; i < RACE_LIVE; i++) flan_dev_reg_note((void *)(uintptr_t)(0x100000 + (uintptr_t)i * 64), 64, 8, "Enemy", 5); if (pthread_create(&w, NULL, race_writer, NULL) != 0) { printf("no writer thread\n"); return 2; } for (int a = 0; a < RACE_ASKS; a++) { int64_t n = flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS, &unread); if (n < 0) refused++; else if (n == 0) zero++; else { int64_t enemies = 0; for (int64_t j = 0; j < n && j < ROWS; j++) if (typelens[j] == 5) enemies = counts[j]; if (enemies == RACE_LIVE) right++; else low++; } } race_stop = 1; pthread_join(w, NULL); printf("zero %d\n", zero); printf("low %d\n", low); printf("answered %d refused %d\n", right, refused); return 0; } int main(int argc, char **argv) { flan_rt_init(argc, argv); if (argc < 2) { fprintf(stderr, "usage: %s cap|names|regfull|regchurn|regrace|regoverflow\n", argv[0]); return 2; } if (strcmp(argv[1], "cap") == 0) return cap(); if (strcmp(argv[1], "names") == 0) return names(); if (strcmp(argv[1], "regfull") == 0) return regfull(); if (strcmp(argv[1], "regchurn") == 0) return regchurn(); if (strcmp(argv[1], "regrace") == 0) return regrace(); if (strcmp(argv[1], "regoverflow") == 0) return regoverflow(); fprintf(stderr, "unknown mode %s\n", argv[1]); return 2; }