@@ -413,3 +413,137 @@ fn a_low_survival_cycle_still_evacuates_and_moves_the_object() {
413413 ) ;
414414 assert ! ( crate :: arena:: pointer_in_nursery( after) ) ;
415415}
416+
417+ // ---------------------------------------------------------------------------
418+ // #7913 interaction: old-page relocation vs a still-DESCRIBED promoted run
419+ // ---------------------------------------------------------------------------
420+
421+ /// #7914 describes a promoted page's object list by its `first_header` address
422+ /// and expands it lazily; #7913 relocates old pages BY DEFAULT. If a page
423+ /// carrying an unexpanded run could be relocated, the deferred expansion would
424+ /// later parse stale addresses — the silent-corruption shape.
425+ ///
426+ /// It cannot, and this pins the mechanism that makes it so:
427+ /// `evacuate_selected_old_pages_collecting` snapshots its source-block
428+ /// occupants through `old_arena_walk_objects_on_pages`, which expands every
429+ /// pending run on every page of the source blocks BEFORE a single object is
430+ /// forwarded. #7913 widened that enumeration from the selected pages to the
431+ /// whole containing source blocks, so it covers strictly more than before.
432+ ///
433+ /// The test is written so it cannot pass by accident: the objects are removed
434+ /// from the eager index first, so a run that is NOT expanded leaves
435+ /// `source_headers` empty, the all-or-nothing guard returns early, and
436+ /// `old_page_moved_objects` reads 0 instead of 3. Deleting the
437+ /// `materialize_promoted_page_runs` call in `old_arena_walk_objects_on_pages`
438+ /// turns this red.
439+ #[ test]
440+ fn old_page_relocation_expands_a_described_run_before_it_moves_anything ( ) {
441+ let _isolation = copying_nursery_isolation_lock ( ) ;
442+ reset_remembered_set ( ) ;
443+ clear_marks ( ) ;
444+ clear_mark_seeds ( ) ;
445+ CONS_PINNED . with ( |s| s. borrow_mut ( ) . clear ( ) ) ;
446+
447+ // Three real old-gen objects, contiguous and linearly parseable — the
448+ // shape a promoted block hands to OLD_ARENA.
449+ let users: Vec < usize > = ( 0 ..3 )
450+ . map ( |_| crate :: arena:: arena_alloc_gc_old ( 64 , 8 , GC_TYPE_OBJECT ) as usize )
451+ . collect ( ) ;
452+ let headers: Vec < ( * mut GcHeader , usize ) > =
453+ users. iter ( ) . map ( |& u| old_test_header_and_size ( u) ) . collect ( ) ;
454+ let first = headers[ 0 ] . 0 as usize ;
455+ let last = headers[ 2 ] . 0 as usize ;
456+ let total: usize = headers. iter ( ) . map ( |& ( _, t) | t) . sum ( ) ;
457+
458+ let mut selected_pages = crate :: fast_hash:: new_ptr_hash_set ( ) ;
459+ for & ( header, size) in & headers {
460+ for ( page, _) in crate :: arena:: old_object_page_overlaps ( header as usize , size) {
461+ selected_pages. insert ( page) ;
462+ }
463+ }
464+ // All three must share one page for the run to describe them as one span;
465+ // 64-byte objects out of a fresh block do, but assert rather than assume.
466+ assert_eq ! (
467+ selected_pages. len( ) ,
468+ 1 ,
469+ "test setup expects one page; a multi-page split would need one run per page"
470+ ) ;
471+ let page = * selected_pages. iter ( ) . next ( ) . unwrap ( ) ;
472+
473+ // Drop the eager index built at birth, then DESCRIBE the same objects.
474+ // From here the run is the only record that this page has occupants.
475+ crate :: arena:: old_arena_page_index_clear_for_tests ( ) ;
476+ crate :: arena:: register_promoted_page_run ( page, first, last, headers. len ( ) , total) ;
477+ assert_eq ! (
478+ crate :: arena:: pending_promoted_page_runs( ) ,
479+ 1 ,
480+ "the page must be DESCRIBED going in, or this test proves nothing"
481+ ) ;
482+
483+ for & ( header, _) in & headers {
484+ unsafe {
485+ ( * header) . gc_flags |= GC_FLAG_MARKED ;
486+ }
487+ }
488+
489+ let mut new_headers = Vec :: new ( ) ;
490+ let mut original_headers = Vec :: new ( ) ;
491+ let moved = evacuate_selected_old_pages_collecting (
492+ & selected_pages,
493+ & mut new_headers,
494+ & mut original_headers,
495+ ) ;
496+
497+ assert_eq ! (
498+ moved. old_page_moved_objects,
499+ headers. len( ) ,
500+ "relocation must see every occupant of a DESCRIBED page. Seeing fewer \
501+ means it relocated a block while some occupants were still only \
502+ described — their memory would be released with live objects in it, \
503+ and the deferred expansion would parse recycled addresses"
504+ ) ;
505+ assert_eq ! (
506+ crate :: arena:: pending_promoted_page_runs( ) ,
507+ 0 ,
508+ "the run must have been consumed by the relocation's own enumeration"
509+ ) ;
510+ for & ( header, _) in & headers {
511+ unsafe {
512+ assert_ne ! (
513+ ( * header) . gc_flags & GC_FLAG_FORWARDED ,
514+ 0 ,
515+ "every described occupant must be forwarded, not just indexed"
516+ ) ;
517+ }
518+ }
519+
520+ release_evacuated_original_forwarding_stubs ( & original_headers) ;
521+ clear_marks ( ) ;
522+ CONS_PINNED . with ( |s| s. borrow_mut ( ) . clear ( ) ) ;
523+ }
524+
525+ /// The second, independent line of defence, stated as a test rather than a
526+ /// comment: a page that has only just been promoted is not defrag-eligible at
527+ /// all, because `register_promoted_page_run` records live bytes and never dead
528+ /// ones, and `old_page_defrag_eligible` requires `dead_bytes > 0`. Dead bytes
529+ /// come only from the old-gen sweep, and the sweep's cycle constructor
530+ /// (`GcCycleState::new_full`) expands every pending run before it starts.
531+ #[ test]
532+ fn a_freshly_described_page_is_not_defrag_eligible ( ) {
533+ let _isolation = copying_nursery_isolation_lock ( ) ;
534+ let user = crate :: arena:: arena_alloc_gc_old ( 64 , 8 , GC_TYPE_OBJECT ) as usize ;
535+ let ( header, size) = old_test_header_and_size ( user) ;
536+ let page = crate :: arena:: old_object_page_overlaps ( header as usize , size) [ 0 ] . 0 ;
537+ crate :: arena:: old_arena_page_index_clear_for_tests ( ) ;
538+ crate :: arena:: register_promoted_page_run ( page, header as usize , header as usize , 1 , size) ;
539+
540+ let meta = crate :: arena:: old_page_meta_for_tests ( page) . expect ( "promotion records page meta" ) ;
541+ assert_eq ! (
542+ meta. dead_bytes, 0 ,
543+ "a promotion records live bytes only; dead bytes are the sweep's to record"
544+ ) ;
545+ assert ! (
546+ !super :: super :: oldgen_defrag:: old_page_defrag_eligible( meta) ,
547+ "a page whose run is still described must not be a defrag candidate"
548+ ) ;
549+ }
0 commit comments