Skip to content

Commit cb0c4ff

Browse files
committed
test(gc2): cover saturated sweep admission
1 parent 9d2213b commit cb0c4ff

3 files changed

Lines changed: 195 additions & 7 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# GC2 saturated Huge admission and TSAN gate repair (2026-07-14)
2+
3+
The plan files are unchanged. This checkpoint adds the last combined stress
4+
case requested by the independent review of SWEEP-stable Huge admission and
5+
repairs a stale boundary assertion exposed by the post-integration TSAN run.
6+
7+
## Saturated Huge admission during SWEEP
8+
9+
`tests/t-gc2-recovery.c` now drives all of the bounded fallback mechanisms to
10+
their limit in one deterministic process-isolated fixture:
11+
12+
- an unmarked Huge userdata has already supplied the SWEEP writer with a stale
13+
retirement ticket;
14+
- all 65,535 representable Huge body-reader tokens are held;
15+
- the 1,024-entry production SSB is full and its only spare node is withheld;
16+
- a real physical SWEEP reclaimer holds `LJ_GC2_SMR_SWEEP_STABLE`; and
17+
- the allocation page is changed to `PROT_NONE` before the late semantic mark.
18+
19+
The late marker can still atomically publish the exact Huge-slot `MARK`, but it
20+
cannot obtain a body reader or enqueue the object. It must return without
21+
reading either the GC header or payload, set the sticky recovery-failure latch,
22+
and pin the activation in `LJ_GC2_ACT_NO_RECLAIM`. The fixture verifies that no
23+
recovery locator was fabricated, releases every reader, and only then wakes the
24+
stale writer. Durable `MARK` remains sufficient for the stale ticket to lose;
25+
the object never enters `FREEING`.
26+
27+
This is deliberately a correctness proof, not a realistic load. The bounded
28+
reader counter and SSB are both exhausted to prove that their simultaneous
29+
failure remains non-dropping and fail-closed.
30+
31+
## Thread-id sentinel boundary
32+
33+
The first post-integration `m4_tsan_drivers` run stopped in
34+
`tests/t-thr-substrate.c` before TSAN reported any race. The fixture still
35+
treated `LJ_THREAD_GCSCAN - 1` as the last process-issued owner id. Since the
36+
addition of the distinct `LJ_THREAD_GCPREP` sentinel, the last valid owner is
37+
instead `LJ_THREAD_GCPREP - 1`.
38+
39+
The runtime allocator already enforced the correct boundary. The fixture now
40+
uses `LJ_THREAD_GCPREP` consistently for both sequential and concurrent
41+
saturation, and explicitly proves that neither reserved sentinel can be passed
42+
to `lj_thr_create` as an owner id.
43+
44+
## Verification
45+
46+
- `m3_gc2_recovery`: passed in the normal helper build and the
47+
assertion/paranoia helper build, including the `PROT_NONE` saturated case.
48+
- `m4_tsan_drivers`: passed after the fixture correction; both the thread/TG
49+
substrate and bounded MPMC channel driver ran against a fully TSAN-instrumented
50+
LuaJIT target with `halt_on_error=1`.
51+
- `m7_ffi_jit_cnew`: passed, including real CNEW, CNEWI, large CNEW traces and
52+
the spawned-thread collection path.
53+
- `m6_jit_flush_thread_stress`: passed at 3 workers, 16 rounds and 32
54+
short-lived threads.
55+
- `m6_jit_flush_thread_heavy_stress`: passed at 4 workers, 96 rounds and 192
56+
short-lived threads.
57+
58+
The wider post-integration ASAN/UBSAN and stress matrix remains a separate
59+
b1.2.0 release-gate activity. Ordinary performance differences remain b1.2.1
60+
work unless they are catastrophic (roughly 100x, runaway resource use, or an
61+
effective hang).

tests/t-gc2-recovery.c

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <signal.h>
1212
#include <stdint.h>
1313
#include <stdio.h>
14+
#include <stdlib.h>
15+
#include <sys/mman.h>
1416
#include <sys/resource.h>
1517
#include <sys/types.h>
1618
#include <sys/wait.h>
@@ -1474,6 +1476,124 @@ static void test_huge_sweep_stable_preadmission_arbitrates_stale_writer(void)
14741476
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
14751477
}
14761478

1479+
static void test_huge_sweep_saturated_reader_failclosed(void)
1480+
{
1481+
pid_t child = fork();
1482+
int status = 0;
1483+
assert(child >= 0);
1484+
if (child == 0) {
1485+
struct rlimit core_limit = {0, 0};
1486+
RecoveryFixture f = recovery_fixture_open();
1487+
RecoverySweepReclaimCtx reclaim = {0};
1488+
LJHugeReader *readers;
1489+
GC2SSBNode *held;
1490+
GCRef *end;
1491+
GCstr *filler;
1492+
GCudata *ud = recovery_make_unlinked_huge_udata(&f);
1493+
LJHugeInfo hi;
1494+
LJGC2ActivationSnap activation;
1495+
pthread_t thread;
1496+
long pagesize;
1497+
void *body_page;
1498+
uint32_t i;
1499+
assert(setrlimit(RLIMIT_CORE, &core_limit) == 0);
1500+
alarm(10);
1501+
1502+
/* Take the stale unmarked retirement snapshot first. Slot readers may be
1503+
** admitted behind its stable TICKET, but retirement itself quite properly
1504+
** refuses to start while any of those body tokens already exists. */
1505+
lj_arena_hugetab_prepare_sweep(&f.tg->huge);
1506+
assert(lj_arena_hugetab_retire(
1507+
&f.tg->huge, ud, ud, 2u, &reclaim.stale) == 1);
1508+
assert((reclaim.stale.flags &
1509+
(LJ_HUGEF_SWEEP_OLD|LJ_HUGEF_RETIRED|LJ_HUGEF_TICKET)) ==
1510+
(LJ_HUGEF_SWEEP_OLD|LJ_HUGEF_RETIRED|LJ_HUGEF_TICKET));
1511+
assert((reclaim.stale.flags & LJ_HUGEF_MARK) == 0);
1512+
1513+
readers = (LJHugeReader *)calloc(0xffffu, sizeof(*readers));
1514+
assert(readers != NULL);
1515+
for (i = 0; i < 0xffffu; i++)
1516+
assert(lj_arena_hugetab_reader_acquire(
1517+
&f.tg->huge, ud, &readers[i], NULL) ==
1518+
LJ_ARENA_HUGE_READER_ACQUIRED);
1519+
assert(lj_arena_hugetab_lookup(&f.tg->huge, ud, &hi) == 1);
1520+
assert(hi.readers == 0xffffu && (hi.flags & LJ_HUGEF_MARK) == 0);
1521+
1522+
/* Remove the only spare node and fill the production SSB. A saturated
1523+
** semantic mark can therefore neither take a body token nor enqueue its
1524+
** retry through the ordinary fast path. */
1525+
held = lj_tg_ssb_free_pop(f.tg);
1526+
assert(held != NULL && lj_tg_ssb_free_acq(f.tg) == NULL);
1527+
end = lj_tg_ssb_end_acq(f.tg);
1528+
lua_pushliteral(f.L, "gc2 saturated sweep admission filler");
1529+
filler = strV(f.L->top - 1);
1530+
for (i = 0; i < TG_GC2_SSB_SLOTS; i++)
1531+
assert(lj_gc2_test_ssb_push(f.g, obj2gco(filler)) == 1);
1532+
assert(lj_tg_ssb_next_acq(f.tg) == end);
1533+
1534+
recovery_publish_sweep_phase(f.g);
1535+
gc2_worker_active_rel(f.g, 1);
1536+
reclaim.g = f.g;
1537+
reclaim.tg = f.tg;
1538+
reclaim.p = ud;
1539+
assert(pthread_create(&thread, NULL,
1540+
recovery_sweep_reclaim_thread, &reclaim) == 0);
1541+
while (la_load32_acq(&reclaim.ready) == 0)
1542+
la_cpu_pause();
1543+
assert(reclaim.entered == 1);
1544+
assert(gc2_smr_reclaiming_acq(f.g) == LJ_GC2_SMR_SWEEP_STABLE);
1545+
1546+
/* Make any GC-header or payload inspection immediately observable. The
1547+
** full-slot MARK_SATURATED CAS is allowed to publish liveness, but without
1548+
** a reader token the semantic marker must return before touching bytes. */
1549+
pagesize = sysconf(_SC_PAGESIZE);
1550+
assert(pagesize > 0 &&
1551+
((uintptr_t)pagesize & ((uintptr_t)pagesize - 1u)) == 0);
1552+
body_page = (void *)((uintptr_t)ud & ~((uintptr_t)pagesize - 1u));
1553+
assert(mprotect(body_page, (size_t)pagesize, PROT_NONE) == 0);
1554+
assert(lj_gc2_markobj(f.g, obj2gco(ud)) == 0);
1555+
assert(gc2_recovery_failed_acq(f.g) == 1u);
1556+
assert(gc2_recovery_items_acq(f.g) == 0);
1557+
assert(gc2_recovery_huge_items_acq(f.g) == 0);
1558+
assert(lj_tg_ssb_next_acq(f.tg) == end);
1559+
activation = lj_gc2_activation_snapshot(&f.g->gc2.activation);
1560+
assert(activation.state == LJ_GC2_ACT_NO_RECLAIM);
1561+
assert(lj_gc2_activation_reclaim_veto(f.g));
1562+
assert(lj_arena_hugetab_lookup(&f.tg->huge, ud, &hi) == 1);
1563+
assert(hi.readers == 0xffffu);
1564+
assert((hi.flags & (LJ_HUGEF_MARK|LJ_HUGEF_RETIRED|
1565+
LJ_HUGEF_FREEING)) == LJ_HUGEF_MARK);
1566+
assert(lj_arena_huge_recovery_state(hi.flags) ==
1567+
LJ_ARENA_RECOVERY_IDLE);
1568+
1569+
/* Remove reader saturation before waking the stale writer so the tokens
1570+
** themselves cannot explain its loss. Durable MARK plus sticky
1571+
** NO_RECLAIM must remain sufficient after the final release. */
1572+
for (i = 0; i < 0xffffu; i++)
1573+
assert(lj_arena_hugetab_reader_release(&readers[i], NULL) ==
1574+
LJ_ARENA_HUGE_READER_RELEASED);
1575+
free(readers);
1576+
assert(lj_arena_hugetab_lookup(&f.tg->huge, ud, &hi) == 1);
1577+
assert(hi.readers == 0u);
1578+
assert((hi.flags & (LJ_HUGEF_MARK|LJ_HUGEF_FREEING)) ==
1579+
LJ_HUGEF_MARK);
1580+
1581+
la_store32_rel(&reclaim.run, 1);
1582+
assert(pthread_join(thread, NULL) == 0);
1583+
assert(la_load32_acq(&reclaim.done) == 1);
1584+
assert(reclaim.result == 0 && reclaim.pending == 1);
1585+
assert(gc2_smr_reclaiming_acq(f.g) == LJ_GC2_SMR_OPEN);
1586+
assert(lj_arena_hugetab_lookup(&f.tg->huge, ud, &hi) == 1);
1587+
assert(hi.readers == 0u);
1588+
assert((hi.flags & (LJ_HUGEF_MARK|LJ_HUGEF_FREEING)) ==
1589+
LJ_HUGEF_MARK);
1590+
alarm(0);
1591+
_exit(0); /* PROT_NONE body and fail-closed activation stay isolated. */
1592+
}
1593+
assert(waitpid(child, &status, 0) == child);
1594+
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
1595+
}
1596+
14771597
static void test_huge_sweep_reader_keeps_pre_destructor_retryable(void)
14781598
{
14791599
pid_t child = fork();
@@ -2028,6 +2148,7 @@ int main(void)
20282148
test_huge_recovery_exact_lane_accounting();
20292149
test_huge_reader_recovery_bypasses_smr_writer();
20302150
test_huge_sweep_stable_preadmission_arbitrates_stale_writer();
2151+
test_huge_sweep_saturated_reader_failclosed();
20312152
test_huge_sweep_reader_keeps_pre_destructor_retryable();
20322153
test_huge_deferred_free_recovery_is_bounded();
20332154
test_empty_huge_lane_is_skipped();
@@ -2040,7 +2161,7 @@ int main(void)
20402161
test_terminal_preflight_preserves_mismatch_locator();
20412162
test_sticky_failure_without_items_is_bounded();
20422163
printf("t-gc2-recovery OK: no-drop SSB/recovery, exact huge-lane "
2043-
"accounting/skip, SWEEP-stable late admission, retryable Huge "
2164+
"accounting/skip, SWEEP-stable late/saturated admission, retryable Huge "
20442165
"destructors, held-reader SMR bypass, bounded terminal free, "
20452166
"cyclic private/metadata edges, free/lifetime races, constructor "
20462167
"overlap, closure vetoes, empty-string SWEEP, sticky failure, and "

tests/t-thr-substrate.c

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,22 @@ int main(void)
174174
uint64_t epoch0;
175175

176176
{
177-
uint32_t counter = LJ_THREAD_GCSCAN - 3u;
177+
uint32_t counter = LJ_THREAD_GCPREP - 3u;
178178
LJThr invalid = {0};
179-
assert(lj_thr_id_alloc(&counter) == LJ_THREAD_GCSCAN - 2u);
180-
assert(lj_thr_id_alloc(&counter) == LJ_THREAD_GCSCAN - 1u);
179+
assert(lj_thr_id_alloc(&counter) == LJ_THREAD_GCPREP - 2u);
180+
assert(lj_thr_id_alloc(&counter) == LJ_THREAD_GCPREP - 1u);
181181
assert(lj_thr_id_alloc(&counter) == 0);
182182
assert(lj_thr_id_alloc(&counter) == 0);
183-
assert(counter == LJ_THREAD_GCSCAN - 1u);
183+
assert(counter == LJ_THREAD_GCPREP - 1u);
184+
counter = LJ_THREAD_GCPREP;
185+
assert(lj_thr_id_alloc(&counter) == 0);
186+
assert(counter == LJ_THREAD_GCPREP);
184187
counter = LJ_THREAD_GCSCAN;
185188
assert(lj_thr_id_alloc(&counter) == 0);
186189
assert(counter == LJ_THREAD_GCSCAN);
190+
invalid.tid = LJ_THREAD_GCPREP;
191+
assert(lj_thr_create(&invalid, id_stress_main, NULL) == EAGAIN);
192+
assert(invalid.tid == 0);
187193
invalid.tid = LJ_THREAD_GCSCAN;
188194
assert(lj_thr_create(&invalid, id_stress_main, NULL) == EAGAIN);
189195
assert(invalid.tid == 0);
@@ -192,7 +198,7 @@ int main(void)
192198
IdStressCtx idctx = {0};
193199
LJThr idthr[ID_STRESS_THREADS] = {{0}};
194200
uint32_t i;
195-
idctx.counter = LJ_THREAD_GCSCAN - 1u - ID_STRESS_COUNT;
201+
idctx.counter = LJ_THREAD_GCPREP - 1u - ID_STRESS_COUNT;
196202
idctx.first = idctx.counter + 1u;
197203
for (i = 0; i < ID_STRESS_THREADS; i++)
198204
assert(lj_thr_create(&idthr[i], id_stress_main, &idctx) == 0);
@@ -202,7 +208,7 @@ int main(void)
202208
for (i = 0; i < ID_STRESS_THREADS; i++)
203209
assert(lj_thr_join(&idthr[i], NULL) == 0);
204210
assert(la_load32_acq(&idctx.successes) == ID_STRESS_COUNT);
205-
assert(la_load32_acq(&idctx.counter) == LJ_THREAD_GCSCAN - 1u);
211+
assert(la_load32_acq(&idctx.counter) == LJ_THREAD_GCPREP - 1u);
206212
for (i = 0; i < ID_STRESS_COUNT; i++)
207213
assert(la_load32_acq(&idctx.seen[i]) == 1);
208214
}

0 commit comments

Comments
 (0)