@@ -1037,11 +1037,21 @@ impl Merger {
10371037 // peers will hand it pointers it can't
10381038 // dereference. Match by resource_name only
10391039 // since the iface differs across the alias.
1040- let found = merged
1040+ // Sort keys for deterministic tie-breaking
1041+ // (LS-A-15).
1042+ let mut keys: Vec < & (
1043+ usize ,
1044+ String ,
1045+ String ,
1046+ ) > = merged
10411047 . handle_tables
1042- . iter ( )
1043- . find ( |( ( _, _, r) , _) | r == rn)
1044- . map ( |( _, ht) | ht) ;
1048+ . keys ( )
1049+ . filter ( |( _, _, r) | r == rn)
1050+ . collect ( ) ;
1051+ keys. sort ( ) ;
1052+ let found = keys
1053+ . first ( )
1054+ . and_then ( |k| merged. handle_tables . get ( * k) ) ;
10451055 if found. is_some ( ) {
10461056 log:: info!(
10471057 "alias-fallback: comp {} mod {} import {}/{} → ht for resource '{}'" ,
@@ -1087,17 +1097,27 @@ impl Merger {
10871097 if self_owns_specific {
10881098 None
10891099 } else {
1090- merged
1100+ // Look up (any-owner, iface, rn) first, then
1101+ // fall back to (any-owner, any-iface, rn).
1102+ // Iterate in sorted-key order so ties are
1103+ // broken deterministically (LS-A-15).
1104+ let mut iface_keys: Vec < & ( usize , String , String ) > = merged
10911105 . handle_tables
1092- . iter ( )
1093- . find ( |( ( _, i, r) , _) | i == iface && r == rn)
1094- . map ( |( _, ht) | ht)
1106+ . keys ( )
1107+ . filter ( |( _, i, r) | i == iface && r == rn)
1108+ . collect ( ) ;
1109+ iface_keys. sort ( ) ;
1110+ iface_keys
1111+ . first ( )
1112+ . and_then ( |k| merged. handle_tables . get ( * k) )
10951113 . or_else ( || {
1096- merged
1114+ let mut any_keys : Vec < & ( usize , String , String ) > = merged
10971115 . handle_tables
1098- . iter ( )
1099- . find ( |( ( _, _, r) , _) | r == rn)
1100- . map ( |( _, ht) | ht)
1116+ . keys ( )
1117+ . filter ( |( _, _, r) | r == rn)
1118+ . collect ( ) ;
1119+ any_keys. sort ( ) ;
1120+ any_keys. first ( ) . and_then ( |k| merged. handle_tables . get ( * k) )
11011121 } )
11021122 }
11031123 } ;
@@ -2921,18 +2941,32 @@ pub(crate) fn component_memory_index(merged: &MergedModule, comp_idx: usize) ->
29212941}
29222942
29232943/// Find the merged function index of a component's cabi_realloc.
2944+ ///
2945+ /// Prefers module 0's realloc (the main module). If module 0 has no
2946+ /// realloc, falls back to the realloc bound to the **lowest** module
2947+ /// index for this component — chosen deterministically rather than via
2948+ /// HashMap iteration order, which would let the hasher state pick a
2949+ /// different module on every run and produce non-reproducible output
2950+ /// (LS-A-15).
29242951pub ( crate ) fn component_realloc_index ( merged : & MergedModule , comp_idx : usize ) -> Option < u32 > {
29252952 // Prefer module 0's realloc (the main module)
29262953 if let Some ( & idx) = merged. realloc_map . get ( & ( comp_idx, 0 ) ) {
29272954 return Some ( idx) ;
29282955 }
2929- // Fallback: any module's realloc for this component
2930- for ( & ( ci, _mi) , & merged_idx) in & merged. realloc_map {
2931- if ci == comp_idx {
2932- return Some ( merged_idx) ;
2933- }
2934- }
2935- None
2956+ // Fallback: pick the smallest module index belonging to this component,
2957+ // deterministically. HashMap.iter() returns entries in hash-seed
2958+ // order, which varies per process; collect-and-sort gives reproducible
2959+ // output and removes the multi-realloc race condition.
2960+ let mut module_idxs: Vec < usize > = merged
2961+ . realloc_map
2962+ . keys ( )
2963+ . filter ( |( ci, _) | * ci == comp_idx)
2964+ . map ( |( _, mi) | * mi)
2965+ . collect ( ) ;
2966+ module_idxs. sort_unstable ( ) ;
2967+ module_idxs
2968+ . first ( )
2969+ . and_then ( |mi| merged. realloc_map . get ( & ( comp_idx, * mi) ) . copied ( ) )
29362970}
29372971
29382972///
@@ -4641,15 +4675,12 @@ mod tests {
46414675
46424676 // ---------------------------------------------------------------
46434677 // LS-A-11 — extended-const truncation in global initializers
4678+ // LS-A-15 — HashMap iteration non-determinism
46444679 //
4645- // Prior to the fix, `convert_init_expr` read only the first
4646- // operator of a global's init expression and emitted just that
4647- // single const, silently dropping any subsequent extended-const
4648- // ops. A global initialized with `(i32.const 100)(i32.const 23)
4649- // i32.add` (intended value 123) was emitted as `(i32.const 100)`.
4680+ // Shared empty-merged fixture used by both regression suites.
46504681 // ---------------------------------------------------------------
46514682
4652- fn empty_merged_for_init_expr ( ) -> MergedModule {
4683+ fn empty_merged_fixture ( ) -> MergedModule {
46534684 MergedModule {
46544685 types : Vec :: new ( ) ,
46554686 imports : Vec :: new ( ) ,
@@ -4679,13 +4710,15 @@ mod tests {
46794710 }
46804711 }
46814712
4713+ // LS-A-11: convert_init_expr must fold multi-op extended-const
4714+ // expressions (was previously truncating to the first operator).
46824715 #[ test]
46834716 fn ls_a_11_convert_init_expr_folds_extended_const_i32_add ( ) {
46844717 // Init expr bytes WITHOUT trailing `end` (the function appends
46854718 // it). Use small operands that fit in single-byte sleb (no sign
46864719 // bit at position 6): i32.const 5, i32.const 10, i32.add → 15.
46874720 let bytes: Vec < u8 > = vec ! [ 0x41 , 5 , 0x41 , 10 , 0x6A ] ;
4688- let merged = empty_merged_for_init_expr ( ) ;
4721+ let merged = empty_merged_fixture ( ) ;
46894722
46904723 let expr = convert_init_expr ( & bytes, 0 , 0 , & merged, & ValType :: I32 ) ;
46914724
@@ -4706,7 +4739,7 @@ mod tests {
47064739 // Regression: the fold path must not change behavior for a
47074740 // simple single-const initializer.
47084741 let bytes: Vec < u8 > = vec ! [ 0x41 , 5 ] ;
4709- let merged = empty_merged_for_init_expr ( ) ;
4742+ let merged = empty_merged_fixture ( ) ;
47104743
47114744 let expr = convert_init_expr ( & bytes, 0 , 0 , & merged, & ValType :: I32 ) ;
47124745 let mut encoded = Vec :: new ( ) ;
@@ -4715,6 +4748,52 @@ mod tests {
47154748 wasm_encoder:: Encode :: encode ( & ConstExpr :: i32_const ( 5 ) , & mut expected) ;
47164749 assert_eq ! ( encoded, expected) ;
47174750 }
4751+
4752+ // LS-A-15: component_realloc_index fallback must be deterministic
4753+ // when multiple modules carry cabi_realloc (was hash-seed dependent).
4754+ #[ test]
4755+ fn ls_a_15_component_realloc_index_picks_lowest_module_deterministically ( ) {
4756+ // Component 0 has reallocs at modules 1 (idx 10), 2 (idx 11),
4757+ // and 3 (idx 12), but no module 0. The function must
4758+ // deterministically pick the realloc bound to the LOWEST module
4759+ // index (module 1 → idx 10), not whatever HashMap happens to
4760+ // iterate first.
4761+ //
4762+ // Rebuilding the HashMap from scratch each iteration gives
4763+ // each instance a fresh hash seed, so iteration order varies
4764+ // across iterations — if the impl picked an arbitrary entry,
4765+ // we'd see more than one observed value.
4766+ let mut observed = std:: collections:: HashSet :: new ( ) ;
4767+ for _ in 0 ..64 {
4768+ let mut merged = empty_merged_fixture ( ) ;
4769+ merged. realloc_map . insert ( ( 0 , 1 ) , 10 ) ;
4770+ merged. realloc_map . insert ( ( 0 , 2 ) , 11 ) ;
4771+ merged. realloc_map . insert ( ( 0 , 3 ) , 12 ) ;
4772+ let got = component_realloc_index ( & merged, 0 ) . unwrap ( ) ;
4773+ observed. insert ( got) ;
4774+ }
4775+ assert_eq ! (
4776+ observed. len( ) ,
4777+ 1 ,
4778+ "component_realloc_index must return a deterministic value \
4779+ across runs; saw {observed:?}",
4780+ ) ;
4781+ assert ! (
4782+ observed. contains( & 10 ) ,
4783+ "lowest module index (1 → realloc 10) must be selected; \
4784+ observed {observed:?}",
4785+ ) ;
4786+ }
4787+
4788+ #[ test]
4789+ fn ls_a_15_component_realloc_index_prefers_module_0 ( ) {
4790+ // Regression: when module 0 has a realloc, the function must
4791+ // return it regardless of other modules in the map.
4792+ let mut merged = empty_merged_fixture ( ) ;
4793+ merged. realloc_map . insert ( ( 0 , 0 ) , 100 ) ;
4794+ merged. realloc_map . insert ( ( 0 , 1 ) , 200 ) ;
4795+ assert_eq ! ( component_realloc_index( & merged, 0 ) , Some ( 100 ) ) ;
4796+ }
47184797}
47194798
47204799// ---------------------------------------------------------------------------
0 commit comments