@@ -2309,13 +2309,14 @@ fn build_relocatable_elf(
23092309 all_code. extend_from_slice ( & func. code ) ;
23102310 }
23112311
2312- let text_section = Section :: new ( ".text" , ElfSectionType :: ProgBits )
2313- . with_flags ( SectionFlags :: ALLOC | SectionFlags :: EXEC )
2314- . with_addr ( 0 ) // Relocatable: addresses start at 0
2315- . with_align ( 4 )
2316- . with_data ( all_code) ;
2317-
2318- elf_builder. add_section ( text_section) ;
2312+ // #354: the `.text` section is built + added LATER (just before the
2313+ // `.bss`/`.data` sections, so it stays section index 4). The per-region
2314+ // split (below) may need to rewrite in-place `R_ARM_ABS32` literal-pool
2315+ // addend words inside `all_code` first — those are REL (the linker computes
2316+ // `S + A` with `A` stored in the data word), so retargeting a static-data
2317+ // reloc from `__synth_wasm_data + C` to `__synth_wasm_seg_K + (C - seg_off)`
2318+ // must patch the word before `.text` is frozen. `all_code` is kept mutable
2319+ // until then.
23192320
23202321 // #237: if any function addresses a wasm static via `__synth_wasm_data`
23212322 // (the --native-pointer-abi path), emit the whole wasm linear memory as one
@@ -2426,8 +2427,141 @@ fn build_relocatable_elf(
24262427 // emission, the two symbol defs, and the `.meld_import_table` index off one
24272428 // `split_linmem_bss` flag so they cannot diverge.
24282429 let split_linmem_bss = native_layout. is_some ( ) && data_segments. is_empty ( ) ;
2430+
2431+ // #354: per-region split for the MIXED case (native-pointer ABI WITH an
2432+ // initialized `(data)` segment). The #345 split only handled the all-zero
2433+ // case; any init segment fell to the one-PROGBITS arm, so a small `.rodata`
2434+ // const at a HIGH linmem offset (gale's stack_push: a 12-byte -ENOMEM const
2435+ // at 65536, above the 64 KiB shadow stack) dragged the whole zero gap into a
2436+ // 65552-byte PROGBITS `.data` (MCU-unshippable). Split per region instead:
2437+ // the zero reservation is `.bss` (NOBITS, `__synth_wasm_data`); each init
2438+ // segment is packed into a small PROGBITS `.data` under its own
2439+ // `__synth_wasm_seg_K` symbol; and every `__synth_wasm_data + C` static-data
2440+ // reloc whose addend C lands in segment K is retargeted to
2441+ // `__synth_wasm_seg_K + (C - seg_off_K)` (symbol + in-place REL addend word).
2442+ //
2443+ // SAFE-BY-CONSTRUCTION GATE: fire only when every init segment sits in the
2444+ // static-data region (offset >= wasm_data_base = the SP-global init, the
2445+ // same boundary the selector uses to classify addresses >= base as static
2446+ // relocs vs `<` as frame offsets) AND every `__synth_wasm_data` reloc is the
2447+ // retargetable literal-pool `Abs32` form (the live form post-#345-step-2).
2448+ // The shadow stack is reached only via the SP register value (dynamic, never
2449+ // a static reloc with an addend in that range), so per-region symbols cannot
2450+ // mis-address anything the selector doesn't already assume separable. If the
2451+ // gate fails, fall back to the one-PROGBITS arm (fat but always correct).
2452+ let wasm_data_base: u32 = native_layout
2453+ . as_ref ( )
2454+ . map ( |ng| ng. sp_init . max ( 0 ) as u32 )
2455+ . unwrap_or ( 0 ) ;
2456+ let all_static_data_abs32 = funcs. iter ( ) . flat_map ( |f| & f. relocations ) . all ( |r| {
2457+ r. symbol != "__synth_wasm_data" || matches ! ( r. kind, synth_core:: backend:: RelocKind :: Abs32 )
2458+ } ) ;
2459+ let mixed_separable = native_layout. is_some ( )
2460+ && !data_segments. is_empty ( )
2461+ && all_static_data_abs32
2462+ && data_segments. iter ( ) . all ( |( off, _) | * off >= wasm_data_base) ;
2463+ // Packed layout of the init segments inside the mixed-case `.data`: each
2464+ // segment 4-aligned in declaration order (i32 loads stay aligned — seg_off
2465+ // is 4-aligned in wasm and the packed start is 4-aligned), then the globals
2466+ // slots. (packed_off per segment, globals packed offset, total .data size).
2467+ let mixed_layout: Option < ( Vec < u32 > , u32 , u32 ) > = if mixed_separable {
2468+ let mut packed = Vec :: with_capacity ( data_segments. len ( ) ) ;
2469+ let mut cur = 0u32 ;
2470+ for ( _off, d) in data_segments {
2471+ cur = cur. next_multiple_of ( 4 ) ;
2472+ packed. push ( cur) ;
2473+ cur += d. len ( ) as u32 ;
2474+ }
2475+ let globals_off = cur. next_multiple_of ( 4 ) ;
2476+ Some ( ( packed, globals_off, globals_off + globals_bytes) )
2477+ } else {
2478+ None
2479+ } ;
2480+ let do_mixed_split = mixed_layout. is_some ( ) ;
2481+ if native_layout. is_some ( ) && !data_segments. is_empty ( ) && !do_mixed_split {
2482+ info ! (
2483+ "Native-pointer linmem: init (data) segment not separable (below base \
2484+ {wasm_data_base} or non-Abs32 static reloc); keeping one PROGBITS \
2485+ .data (correct, not per-region split) — #354 fallback"
2486+ ) ;
2487+ }
2488+
2489+ // #354: retarget map + in-place REL addend patch for the mixed case. Keyed
2490+ // by (func index, reloc offset) -> (new symbol, new addend). The addend C is
2491+ // read from the in-place `.text` literal word (Abs32 is REL — `S + A`).
2492+ let mut retarget: HashMap < ( usize , u32 ) , ( String , i32 ) > = HashMap :: new ( ) ;
2493+ if do_mixed_split {
2494+ for ( i, func) in funcs. iter ( ) . enumerate ( ) {
2495+ for reloc in & func. relocations {
2496+ if reloc. symbol != "__synth_wasm_data"
2497+ || !matches ! ( reloc. kind, synth_core:: backend:: RelocKind :: Abs32 )
2498+ {
2499+ continue ;
2500+ }
2501+ let pos = ( func_offsets[ i] + reloc. offset ) as usize ;
2502+ if pos + 4 > all_code. len ( ) {
2503+ continue ;
2504+ }
2505+ let c = u32:: from_le_bytes ( [
2506+ all_code[ pos] ,
2507+ all_code[ pos + 1 ] ,
2508+ all_code[ pos + 2 ] ,
2509+ all_code[ pos + 3 ] ,
2510+ ] ) ;
2511+ if let Some ( k) = data_segments
2512+ . iter ( )
2513+ . position ( |( off, d) | c >= * off && c < * off + d. len ( ) as u32 )
2514+ {
2515+ let new_addend = ( c - data_segments[ k] . 0 ) as i32 ;
2516+ retarget. insert (
2517+ ( i, reloc. offset ) ,
2518+ ( format ! ( "__synth_wasm_seg_{k}" ) , new_addend) ,
2519+ ) ;
2520+ all_code[ pos..pos + 4 ] . copy_from_slice ( & new_addend. to_le_bytes ( ) ) ;
2521+ }
2522+ }
2523+ }
2524+ }
2525+
2526+ // #354/#345: build + add `.text` (section index 4) now — after any in-place
2527+ // addend patch and before the `.bss`/`.data` sections, so `.text` keeps its
2528+ // index and the patched literal words are what ship.
2529+ let text_section = Section :: new ( ".text" , ElfSectionType :: ProgBits )
2530+ . with_flags ( SectionFlags :: ALLOC | SectionFlags :: EXEC )
2531+ . with_addr ( 0 )
2532+ . with_align ( 4 )
2533+ . with_data ( all_code) ;
2534+ elf_builder. add_section ( text_section) ;
2535+
24292536 if emit_wasm_data {
2430- if split_linmem_bss {
2537+ if do_mixed_split {
2538+ // #354: zero reservation -> NOBITS `.bss` (section 5),
2539+ // `__synth_wasm_data` = 0; init segments packed into a small PROGBITS
2540+ // `.data` (section 6), each under its own `__synth_wasm_seg_K`.
2541+ let ( packed, globals_off, data_size) = mixed_layout. as_ref ( ) . unwrap ( ) ;
2542+ let bss = Section :: new ( ".bss" , ElfSectionType :: NoBits )
2543+ . with_flags ( SectionFlags :: ALLOC | SectionFlags :: WRITE )
2544+ . with_addr ( 0 )
2545+ . with_align ( 4 )
2546+ . with_size ( used_extent) ;
2547+ elf_builder. add_section ( bss) ;
2548+ let mut blob = vec ! [ 0u8 ; * data_size as usize ] ;
2549+ for ( ( _off, d) , & poff) in data_segments. iter ( ) . zip ( packed. iter ( ) ) {
2550+ blob[ poff as usize ..poff as usize + d. len ( ) ] . copy_from_slice ( d) ;
2551+ }
2552+ if let Some ( ng) = & native_layout {
2553+ for ( idx, init) in & ng. globals {
2554+ let at = ( * globals_off + idx * 4 ) as usize ;
2555+ blob[ at..at + 4 ] . copy_from_slice ( & init. to_le_bytes ( ) ) ;
2556+ }
2557+ }
2558+ let data = Section :: new ( ".data" , ElfSectionType :: ProgBits )
2559+ . with_flags ( SectionFlags :: ALLOC | SectionFlags :: WRITE )
2560+ . with_addr ( 0 )
2561+ . with_align ( 4 )
2562+ . with_data ( blob) ;
2563+ elf_builder. add_section ( data) ;
2564+ } else if split_linmem_bss {
24312565 // #345: zero-init linmem reservation → NOBITS `.bss` (section 5).
24322566 let bss = Section :: new ( ".bss" , ElfSectionType :: NoBits )
24332567 . with_flags ( SectionFlags :: ALLOC | SectionFlags :: WRITE )
@@ -2550,12 +2684,31 @@ fn build_relocatable_elf(
25502684 elf_builder. add_symbol ( data_sym) ;
25512685 sym_count += 1 ;
25522686 sym_indices. insert ( "__synth_wasm_data" . to_string ( ) , sym_count) ;
2553- // #237/#345: the globals slot region. In the non-split (PROGBITS) case
2554- // it sits right after the used extent inside section 5. In the #345
2555- // split case the slots live in their own PROGBITS `.data` (section 6),
2556- // at value 0.
2687+ // #354: per-region segment symbols. In the mixed split each init
2688+ // segment is packed into the `.data` section (index 6) at its packed
2689+ // offset; `__synth_wasm_seg_K` is its base, and the retargeted static
2690+ // relocs resolve against it (+ the rewritten in-place addend).
2691+ if let Some ( ( packed, _goff, _sz) ) = & mixed_layout {
2692+ for ( k, & poff) in packed. iter ( ) . enumerate ( ) {
2693+ let seg_sym = Symbol :: new ( & format ! ( "__synth_wasm_seg_{k}" ) )
2694+ . with_value ( poff)
2695+ . with_binding ( SymbolBinding :: Global )
2696+ . with_type ( SymbolType :: Object )
2697+ . with_section ( 6 ) ;
2698+ elf_builder. add_symbol ( seg_sym) ;
2699+ sym_count += 1 ;
2700+ sym_indices. insert ( format ! ( "__synth_wasm_seg_{k}" ) , sym_count) ;
2701+ }
2702+ }
2703+ // #237/#345/#354: the globals slot region. In the non-split (PROGBITS)
2704+ // case it sits right after the used extent inside section 5. In the #345
2705+ // all-zero split and the #354 mixed split the slots live in the PROGBITS
2706+ // `.data` (section 6) — at value 0 (#345) or after the packed segments
2707+ // (#354).
25572708 if native_layout. is_some ( ) {
2558- let ( gv, gsec) = if split_linmem_bss {
2709+ let ( gv, gsec) = if let Some ( ( _packed, goff, _sz) ) = & mixed_layout {
2710+ ( * goff, 6 )
2711+ } else if split_linmem_bss {
25592712 ( 0 , 6 )
25602713 } else {
25612714 ( used_extent, 5 )
@@ -2613,7 +2766,14 @@ fn build_relocatable_elf(
26132766 for ( i, func) in funcs. iter ( ) . enumerate ( ) {
26142767 let func_base = func_offsets[ i] ;
26152768 for reloc in & func. relocations {
2616- let sym_idx = sym_indices[ & reloc. symbol ] ;
2769+ // #354: a static-data reloc retargeted to a per-region segment
2770+ // symbol resolves against that symbol; all others against their
2771+ // original symbol.
2772+ let sym_name = retarget
2773+ . get ( & ( i, reloc. offset ) )
2774+ . map ( |( s, _) | s. as_str ( ) )
2775+ . unwrap_or ( reloc. symbol . as_str ( ) ) ;
2776+ let sym_idx = sym_indices[ sym_name] ;
26172777 // #237: map the relocation kind. BL calls → R_ARM_THM_CALL; the
26182778 // symbol-relative static-data MOVW/MOVT → R_ARM_MOVW_ABS_NC/MOVT_ABS.
26192779 let reloc_type = match reloc. kind {
@@ -4004,6 +4164,134 @@ mod tests {
40044164 ) ;
40054165 }
40064166
4167+ /// #354: a native-pointer module with an initialized `(data)` segment at a
4168+ /// HIGH linmem offset (above the shadow stack) must NOT ship the whole
4169+ /// `[0, used_extent)` image as one PROGBITS `.data` (65552 bytes for gale's
4170+ /// stack_push). It splits per region: the zero reservation is NOBITS `.bss`
4171+ /// (`__synth_wasm_data`), the init segment is packed into a tiny PROGBITS
4172+ /// `.data` under `__synth_wasm_seg_0`, and the static-data reloc is
4173+ /// retargeted (`__synth_wasm_data + C` -> `__synth_wasm_seg_0 + (C-seg_off)`,
4174+ /// symbol AND in-place REL addend word).
4175+ #[ test]
4176+ fn mixed_high_offset_segment_splits_per_region_354 ( ) {
4177+ use object:: Endianness ;
4178+ use object:: read:: elf:: { FileHeader , SectionHeader } ;
4179+
4180+ // The const is accessed by a literal-pool Abs32 load whose in-place word
4181+ // is the absolute linmem offset C = 65544 (8 bytes into a segment based
4182+ // at 65536 — gale's `\f4\ff\ff\ff` = -12/-ENOMEM at offset+8).
4183+ const C : u32 = 65_544 ;
4184+ const SEG_OFF : u32 = 65_536 ;
4185+ let mut code = vec ! [ 0u8 ; 4 ] ;
4186+ code[ 0 ..4 ] . copy_from_slice ( & C . to_le_bytes ( ) ) ;
4187+ let func = ElfFunction {
4188+ name : "stack_push_decide" . to_string ( ) ,
4189+ wasm_index : 0 ,
4190+ code,
4191+ relocations : vec ! [ synth_core:: backend:: CodeRelocation {
4192+ offset: 0 ,
4193+ symbol: "__synth_wasm_data" . to_string( ) ,
4194+ kind: synth_core:: backend:: RelocKind :: Abs32 ,
4195+ } ] ,
4196+ } ;
4197+ // 12-byte init segment at the high offset, above the shadow stack.
4198+ let seg: Vec < u8 > = vec ! [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0xf4 , 0xff , 0xff , 0xff ] ;
4199+ let data_segments = vec ! [ ( SEG_OFF , seg) ] ;
4200+ let native = NativeGlobalsLayout {
4201+ globals : vec ! [ ( 0 , 65_536 ) ] ,
4202+ sp_init : 65_536 ,
4203+ } ;
4204+
4205+ let elf = build_relocatable_elf ( & [ func] , & [ ] , & data_segments, 131_072 , Some ( native) )
4206+ . expect ( "#354: mixed-case object builds" ) ;
4207+
4208+ let header = object:: elf:: FileHeader32 :: < Endianness > :: parse ( & * elf) . expect ( "valid ELF32" ) ;
4209+ let endian = header. endian ( ) . expect ( "endian" ) ;
4210+ let sections = header. sections ( endian, & * elf) . expect ( "sections" ) ;
4211+
4212+ let mut bss_size: Option < u64 > = None ;
4213+ let mut bss_is_nobits = false ;
4214+ let mut data_size: Option < u64 > = None ;
4215+ let mut text_data: Vec < u8 > = Vec :: new ( ) ;
4216+ for section in sections. iter ( ) {
4217+ let name = sections
4218+ . section_name ( endian, section)
4219+ . map ( |n| String :: from_utf8_lossy ( n) . into_owned ( ) )
4220+ . unwrap_or_default ( ) ;
4221+ match name. as_str ( ) {
4222+ ".bss" => {
4223+ bss_is_nobits = section. sh_type ( endian) == object:: elf:: SHT_NOBITS ;
4224+ bss_size = Some ( section. sh_size ( endian) . into ( ) ) ;
4225+ }
4226+ ".data" => data_size = Some ( section. sh_size ( endian) . into ( ) ) ,
4227+ ".text" => {
4228+ text_data = section. data ( endian, & * elf) . unwrap_or_default ( ) . to_vec ( ) ;
4229+ }
4230+ _ => { }
4231+ }
4232+ }
4233+
4234+ // The zero reservation is a NOBITS .bss spanning the used extent.
4235+ let bss = bss_size. expect ( "#354: a .bss reservation must be present" ) ;
4236+ assert ! (
4237+ bss_is_nobits,
4238+ "#354: the zero reservation must be SHT_NOBITS"
4239+ ) ;
4240+ assert ! (
4241+ bss >= 65_536 ,
4242+ "#354: .bss spans the zero gap (got {bss} bytes)"
4243+ ) ;
4244+ // The PROGBITS .data is bounded to the packed segment + globals, NOT the
4245+ // 64 KiB image.
4246+ let data = data_size. expect ( "#354: a small PROGBITS .data must be present" ) ;
4247+ assert ! (
4248+ data < 256 ,
4249+ "#354: .data is bounded to the init bytes, not the 64 KiB image (got {data} bytes)"
4250+ ) ;
4251+
4252+ // The static-data reloc is retargeted to __synth_wasm_seg_0 — verified
4253+ // via the stable high-level API (avoids the low-level Sym trait, which is
4254+ // ambiguous with two `object` versions in the tree).
4255+ {
4256+ use object:: { Object , ObjectSection , ObjectSymbol } ;
4257+ let file = object:: File :: parse ( & * elf) . expect ( "#354: parse ELF" ) ;
4258+ assert ! (
4259+ file. symbols( ) . any( |s| s. name( ) == Ok ( "__synth_wasm_seg_0" ) ) ,
4260+ "#354: __synth_wasm_seg_0 symbol must be defined"
4261+ ) ;
4262+ let mut retargeted = false ;
4263+ for section in file. sections ( ) {
4264+ for ( _off, rel) in section. relocations ( ) {
4265+ if let object:: RelocationTarget :: Symbol ( idx) = rel. target ( )
4266+ && file
4267+ . symbol_by_index ( idx)
4268+ . ok ( )
4269+ . and_then ( |s| s. name ( ) . ok ( ) . map ( |n| n == "__synth_wasm_seg_0" ) )
4270+ . unwrap_or ( false )
4271+ {
4272+ retargeted = true ;
4273+ }
4274+ }
4275+ }
4276+ assert ! (
4277+ retargeted,
4278+ "#354: the static-data reloc must retarget to __synth_wasm_seg_0"
4279+ ) ;
4280+ }
4281+
4282+ // The in-place REL addend word in .text was rewritten C -> C - seg_off.
4283+ assert ! (
4284+ text_data. len( ) >= 4 ,
4285+ "#354: .text must hold the pooled word"
4286+ ) ;
4287+ let patched = u32:: from_le_bytes ( [ text_data[ 0 ] , text_data[ 1 ] , text_data[ 2 ] , text_data[ 3 ] ] ) ;
4288+ assert_eq ! (
4289+ patched,
4290+ C - SEG_OFF ,
4291+ "#354: in-place addend rewritten to C-seg_off (8); link computes seg0_base+8 = the const"
4292+ ) ;
4293+ }
4294+
40074295 /// Without a `call_indirect`, table entries are NOT force-included — a module
40084296 /// that never dispatches indirectly keeps the tight direct-call closure.
40094297 #[ test]
0 commit comments