gc: Fix ABA race in concurrent page-pool sweep (#62162)
* gc: fix ABA race in concurrent page-pool sweep via relaxed occupancy count
Under concurrent page sweeping (`--gcthreads=N,1`), `gc_free_pages` popped every
page off `global_page_pool_lazily_freed`, held the `default_collect_interval`
warm cache in a temporary stack, then re-pushed those kept pages back onto
`global_page_pool_lazily_freed`. Mutator threads pop that same lock-free stack
in `jl_gc_alloc_page` at the same time. Re-pushing a just-popped node while
concurrent pops are in flight reintroduces it mid-pop and violates the no-ABA
invariant the Treiber stack relies on (documented in gc-stock.h): the CAS in a
concurrent `pop_lf_back` can succeed against a stale head whose `next` now points
into an already-handed-out page, so one physical page is handed to two owners.
One owner then `madvise`-frees a page still backing a live object, zeroing its
type tag, and the next mark loop aborts on the corrupted object (also observed as
a use-after-free crash in `jl_lookup_generic_`).
Replace the pop/hold/re-push cycle with a relaxed approximate occupancy counter,
`global_page_pool_lazily_freed_n`, maintained with relaxed inc/dec at the push
site (`gc_sweep_page`) and the pop sites (`jl_gc_alloc_page`, `gc_free_pages`).
`gc_free_pages` now reads the estimated occupancy up front and reclaims only the
surplus above `default_collect_interval / GC_PAGE_SZ` pages, popping them once
and reclaiming them into `global_page_pool_freed` (which is only ever pushed here
and popped by the allocator, so it never makes a popped node reappear). The warm
cache is simply left in place on the stack, so the temporary stack and the
re-push -- the source of the ABA -- are gone entirely, while the
`default_collect_interval` warm-cache behavior is preserved.
Co-authored-by: GitHub Copilot <copilot@github.com>
* gc: add ABA race reproducer / general multithreaded gc stress test
* gc: test portability to win-mingw32, i686 linux
---------
Co-authored-by: GitHub Copilot <copilot@github.com>