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+
14771597static 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 "
0 commit comments