From 0fd3d5a9108279e0c3d829ceff229add28325a37 Mon Sep 17 00:00:00 2001 From: Sylvain Vieujot Date: Mon, 31 Aug 2026 23:50:11 +0400 Subject: [PATCH 1/6] tolerate undefined data symbols in emitted modules An input module can carry undefined data symbols: rustc incremental builds sometimes leave references to promoted anonymous globals (anon..N.llvm.) whose content-derived names changed between compilations, and wasm-ld links them under --allow-undefined. The emit phase treated any such symbol in a module's included set as fatal. An undefined symbol has no definition to place or relocate, so skip it with a warning and keep its references exactly as the linker resolved them - the same treatment the zero-sized symbol case already gets. --- crates/wasm_split_cli/src/emit.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/wasm_split_cli/src/emit.rs b/crates/wasm_split_cli/src/emit.rs index eed4dbe..a5448b3 100644 --- a/crates/wasm_split_cli/src/emit.rs +++ b/crates/wasm_split_cli/src/emit.rs @@ -12,7 +12,7 @@ use crate::{ split_point::{SplitModuleIdentifier, SplitProgramInfo}, util::{wasm_data_len, wasm_data_start}, }; -use eyre::{anyhow, bail, Context, Result}; +use eyre::{bail, Context, Result}; use tracing::{trace, warn}; use wasm_encoder::{reencode::Reencode, ConstExpr, EntityType, ProducersField, ProducersSection}; use wasmparser::{ @@ -374,10 +374,18 @@ impl DataEmitInfo { .. } = input_module.reloc_info.symbols[symbol_index] else { - // Not sure how to emit an *undefined* data symbol - return Some(Err(anyhow!( - "Expected data symbol dep node to ref to defined data symbol" - ))); + // An undefined data symbol carries no definition to + // place or relocate. Linkers accept these under + // --allow-undefined (rustc incremental builds can + // leave references to promoted anonymous globals + // whose content-derived names changed), so keep the + // references exactly as the linker resolved them. + if let SymbolInfo::Data { name, .. } = + input_module.reloc_info.symbols[symbol_index] + { + warn!("Undefined data symbol {name:?} in included set; leaving its references unchanged."); + } + return None; }; if def_data.size == 0 { // We don't care about zero-sized symbols. From 7059165b0452f724be082d1c9cf21df92e1fec71 Mon Sep 17 00:00:00 2001 From: Sylvain Vieujot Date: Tue, 1 Sep 2026 08:12:25 +0400 Subject: [PATCH 2/6] add an end-to-end test for undefined data symbols Builds a minimal splittable module whose symbol table carries an undefined data symbol referenced by a R_WASM_MEMORY_ADDR_LEB relocation from a function kept in the main module - the shape rustc incremental builds produce via renamed promoted anonymous globals, linked by wasm-ld under --allow-undefined. Asserts the transform succeeds, that the defined data symbol next to it is still relocated to the output segment base (proving the relocation plumbing in the test attaches to the dependency pass), and that the undefined reference keeps the linker-resolved address 0. Fails against the previous emit code with "Expected data symbol dep node to ref to defined data symbol". --- .../tests/undefined_data_symbol_end_to_end.rs | 415 ++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs diff --git a/crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs b/crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs new file mode 100644 index 0000000..e925732 --- /dev/null +++ b/crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs @@ -0,0 +1,415 @@ +//! Builds a minimal splittable wasm whose symbol table carries an +//! *undefined* data symbol - `definition: None`, `WASM_SYM_UNDEFINED` - +//! that is referenced by a `R_WASM_MEMORY_ADDR_LEB` relocation from a +//! function kept in the main module. rustc produces this shape with +//! `incremental` + multiple codegen units: a reused object can keep +//! references to a promoted anonymous global (`anon...llvm.`) +//! whose content-derived name changed in a recompiled unit, and wasm-ld +//! links the dangling reference under `--allow-undefined`, resolving it +//! to address 0. +//! +//! Runs the full `wasm_split_cli_support::transform()` pipeline on it and +//! asserts: +//! +//! 1. the transform succeeds - the undefined symbol has no definition +//! to place or relocate, so it must be skipped, not treated as +//! fatal; and +//! 2. the reference to the *defined* data symbol next to it is still +//! relocated to the output segment's base - proving the relocation +//! plumbing in this test attaches to the dependency pass (a broken +//! reloc encoding would make assertion 1 pass vacuously); and +//! 3. the reference to the undefined symbol keeps the address the +//! linker resolved (0), untouched by any fixup. +//! +//! See #59. + +use std::borrow::Cow; + +use wasm_encoder::{ + CodeSection, ConstExpr, CustomSection, DataSection, DataSymbolDefinition, Encode, ExportKind, + ExportSection, Function, FunctionSection, ImportSection, Instruction, MemorySection, + MemoryType, Module, RefType, SymbolTable, TableSection, TableType, TypeSection, +}; +use wasmparser::{DataKind, Operator, Parser, Payload, TypeRef}; + +use wasm_split_cli_support::{transform, Options}; + +#[path = "../src/magic_constants.rs"] +mod magic_constants; + +/// Input layout (with 1 import): 0 = placeholder import, 1 = split_body, +/// 2 = main, 3 = reads_data. +const SPLIT_BODY_IDX: u32 = 1; +const MAIN_IDX: u32 = SPLIT_BODY_IDX + 1; +const READS_DATA_IDX: u32 = MAIN_IDX + 1; +const READS_DATA_EXPORT: &str = "reads_data"; + +/// Symbol table layout. +const DEFINED_DATA_SYM: u32 = 1; +const UNDEFINED_DATA_SYM: u32 = 2; + +const SEGMENT_BASE: i32 = 1024; +const SEGMENT_BYTES: &[u8] = b"0123456789abcdef"; + +/// `i32.const` with a linker-style 5-byte padded LEB immediate, the shape +/// relocation fixups patch in place. +fn padded_i32_const(value: u32) -> [u8; 6] { + let mut bytes = [0x41, 0x80, 0x80, 0x80, 0x80, 0x00]; + bytes[1] |= (value & 0x7f) as u8; + bytes[2] |= ((value >> 7) & 0x7f) as u8; + bytes[3] |= ((value >> 14) & 0x7f) as u8; + bytes[4] |= ((value >> 21) & 0x7f) as u8; + bytes[5] = ((value >> 28) & 0x0f) as u8; + bytes +} + +fn uleb(mut value: u64, out: &mut Vec) { + loop { + let byte = (value & 0x7f) as u8; + value >>= 7; + if value == 0 { + out.push(byte); + return; + } + out.push(byte | 0x80); + } +} + +/// The module, optionally with a `reloc.CODE` custom section appended. +/// Two-pass construction: the first pass (without relocations) is byte +/// identical up to the appended section, so offsets measured on it stay +/// valid for the second. +fn build_input_wasm(reloc_code: Option<&[u8]>) -> Vec { + let mut module = Module::new(); + + let mut types = TypeSection::new(); + types.ty().function([], []); + module.section(&types); + + const SPLIT_NAME: &str = "testsplit"; + const SPLIT_HASH: &str = "00000000000000000000000000000000"; + let import_name = format!("__wasm_split_00{SPLIT_NAME}00_import_{SPLIT_HASH}"); + let export_name = format!("__wasm_split_00{SPLIT_NAME}00_export_{SPLIT_HASH}"); + + let mut imports = ImportSection::new(); + imports.import( + magic_constants::PLACEHOLDER_IMPORT_MODULE, + &import_name, + wasm_encoder::EntityType::Function(0), + ); + module.section(&imports); + + let mut functions = FunctionSection::new(); + for _ in 1..=READS_DATA_IDX { + functions.function(0); + } + module.section(&functions); + + let mut tables = TableSection::new(); + tables.table(TableType { + element_type: RefType::FUNCREF, + minimum: 1, + maximum: Some(1), + table64: false, + shared: false, + }); + module.section(&tables); + + let mut memories = MemorySection::new(); + memories.memory(MemoryType { + minimum: 1, + maximum: None, + memory64: false, + shared: false, + page_size_log2: None, + }); + module.section(&memories); + + let mut exports = ExportSection::new(); + exports.export(&export_name, ExportKind::Func, SPLIT_BODY_IDX); + exports.export("main", ExportKind::Func, MAIN_IDX); + exports.export(READS_DATA_EXPORT, ExportKind::Func, READS_DATA_IDX); + exports.export("__indirect_function_table", ExportKind::Table, 0); + exports.export("memory", ExportKind::Memory, 0); + module.section(&exports); + + let mut code = CodeSection::new(); + + let mut split_body = Function::new([]); + split_body.instruction(&Instruction::End); + code.function(&split_body); + + let mut main = Function::new([]); + main.instruction(&Instruction::End); + code.function(&main); + + // reads_data: two padded-LEB `i32.const` sites, one per relocation. + // Raw bytes rather than the instruction API: the immediates must be + // 5-byte padded LEBs for an in-place fixup, and the encoder emits + // minimal ones. + let mut reads_data = vec![0x00]; // no locals + reads_data.extend_from_slice(&padded_i32_const(SEGMENT_BASE as u32)); + reads_data.push(0x1a); // drop + reads_data.extend_from_slice(&padded_i32_const(0)); + reads_data.push(0x1a); // drop + reads_data.push(0x0b); // end + code.raw(&reads_data); + + module.section(&code); + + let mut data = DataSection::new(); + data.active( + 0, + &ConstExpr::i32_const(SEGMENT_BASE), + SEGMENT_BYTES.iter().copied(), + ); + module.section(&data); + + let mut sym_tab = SymbolTable::new(); + sym_tab.table( + SymbolTable::WASM_SYM_BINDING_LOCAL, + 0, + Some("__indirect_function_table"), + ); + // Symbol 1: an ordinary defined data symbol covering the segment. + sym_tab.data( + 0, + "defined_data", + Some(DataSymbolDefinition { + index: 0, + offset: 0, + size: SEGMENT_BYTES.len() as u32, + }), + ); + // Symbol 2: the undefined data symbol - no definition part. The name + // mirrors what rustc incremental builds leave behind. + sym_tab.data( + SymbolTable::WASM_SYM_UNDEFINED, + "anon.baadf00dcafe.7.llvm.1234567890", + None, + ); + // Hand-built "linking" custom section: wasm-encoder's LinkingSection + // has no segment-info API yet, and the reader requires one entry per + // data segment. Layout: version, WASM_SEGMENT_INFO subsection, then + // the encoded symbol table subsection. + let mut linking_data = Vec::new(); + uleb(2, &mut linking_data); // linking metadata version + let mut segment_info = Vec::new(); + uleb(1, &mut segment_info); // one segment + uleb(".data".len() as u64, &mut segment_info); + segment_info.extend_from_slice(b".data"); + uleb(0, &mut segment_info); // alignment (log2) + uleb(0, &mut segment_info); // flags + linking_data.push(0x05); // WASM_SEGMENT_INFO + uleb(segment_info.len() as u64, &mut linking_data); + linking_data.extend_from_slice(&segment_info); + sym_tab.encode(&mut linking_data); + module.section(&CustomSection { + name: Cow::Borrowed("linking"), + data: Cow::Borrowed(&linking_data), + }); + + // wasm-split marker: [tag u8][payload_len uleb][payload]. (see wasm_split/../marker.rs) + let ws_payload = [1u8, 1u8, 1u8]; + module.section(&CustomSection { + name: Cow::Borrowed(magic_constants::LINK_SECTION), + data: Cow::Borrowed(&ws_payload), + }); + + if let Some(payload) = reloc_code { + module.section(&CustomSection { + name: Cow::Borrowed("reloc.CODE"), + data: Cow::Borrowed(payload), + }); + } + + module.finish() +} + +/// `(section_index, contents_range)` of the code section, counting section +/// indices the way the relocation reader does: one per section payload, in +/// module order. +fn locate_code_section(wasm: &[u8]) -> (u64, std::ops::Range) { + let mut section_index: u64 = 0; + for payload in Parser::new(0).parse_all(wasm) { + let payload = payload.expect("valid wasm"); + if let Payload::CodeSectionStart { range, .. } = &payload { + return (section_index, range.start as u64..range.end as u64); + } + if payload.as_section().is_some() { + section_index += 1; + } + } + panic!("module has no code section"); +} + +/// Byte offset of `pattern` in `haystack`; the pattern must be unique. +fn locate_unique(haystack: &[u8], pattern: &[u8]) -> u64 { + let mut positions = haystack + .windows(pattern.len()) + .enumerate() + .filter(|(_, w)| *w == pattern) + .map(|(i, _)| i as u64); + let position = positions.next().expect("pattern not found"); + assert!(positions.next().is_none(), "pattern is not unique"); + position +} + +/// `(func_export_index, defined_func_index)`. +fn locate_export(wasm: &[u8], export_name: &str) -> (u32, usize) { + let mut import_count: usize = 0; + let mut target: Option = None; + for payload in Parser::new(0).parse_all(wasm) { + match payload.expect("valid wasm") { + Payload::ImportSection(reader) => { + for imp in reader { + let Ok(wasmparser::Imports::Single(_, imp)) = imp else { + panic!("valid import") + }; + if matches!(imp.ty, TypeRef::Func(_)) { + import_count += 1; + } + } + } + Payload::ExportSection(reader) => { + for exp in reader { + let exp = exp.expect("valid export"); + if exp.name == export_name { + target = Some(exp.index); + break; + } + } + } + _ => {} + } + } + let idx = target.unwrap_or_else(|| panic!("export {export_name:?} not found")); + let defined = (idx as usize).checked_sub(import_count).unwrap_or_else(|| { + panic!( + "export {export_name:?} points at imported index {idx}, \ + not a defined function (imports = {import_count})" + ) + }); + (idx, defined) +} + +fn nth_defined_body<'a>(wasm: &'a [u8], nth: usize) -> wasmparser::FunctionBody<'a> { + Parser::new(0) + .parse_all(wasm) + .filter_map(|p| match p.expect("valid wasm") { + Payload::CodeSectionEntry(body) => Some(body), + _ => None, + }) + .nth(nth) + .unwrap_or_else(|| panic!("module has fewer than {} defined functions", nth + 1)) +} + +/// The immediates of every `i32.const` in a body, in order. +fn i32_const_immediates(body: &wasmparser::FunctionBody<'_>) -> Vec { + let mut reader = body.get_operators_reader().expect("operators"); + let mut values = Vec::new(); + while !reader.eof() { + if let Operator::I32Const { value } = reader.read().expect("op") { + values.push(value); + } + } + values +} + +/// Base address of the module's single active data segment. +fn single_segment_base(wasm: &[u8]) -> i32 { + let mut base: Option = None; + for payload in Parser::new(0).parse_all(wasm) { + if let Payload::DataSection(reader) = payload.expect("valid wasm") { + for segment in reader { + let segment = segment.expect("valid data segment"); + let DataKind::Active { offset_expr, .. } = segment.kind else { + panic!("expected an active data segment"); + }; + let mut ops = offset_expr.get_operators_reader(); + let Operator::I32Const { value } = ops.read().expect("op") else { + panic!("expected an i32.const segment offset"); + }; + assert!(base.is_none(), "expected exactly one data segment"); + base = Some(value); + } + } + } + base.expect("output module has no data segment - did the relocations attach?") +} + +#[test] +fn undefined_data_symbol_is_skipped_not_fatal() { + tracing_subscriber::fmt::init(); + + // Pass 1: measure the relocation targets on a module without the + // reloc section (appending it does not shift earlier offsets). + let unrelocated = build_input_wasm(None); + let (code_section_index, code_range) = locate_code_section(&unrelocated); + let defined_site = + locate_unique(&unrelocated, &padded_i32_const(SEGMENT_BASE as u32)) + 1 - code_range.start; + let undefined_site = locate_unique(&unrelocated, &padded_i32_const(0)) + 1 - code_range.start; + + // R_WASM_MEMORY_ADDR_LEB entries against the defined and the + // undefined data symbol. + let mut reloc = Vec::new(); + uleb(code_section_index, &mut reloc); + uleb(2, &mut reloc); + for (site, symbol) in [ + (defined_site, DEFINED_DATA_SYM), + (undefined_site, UNDEFINED_DATA_SYM), + ] { + reloc.push(0x03); // R_WASM_MEMORY_ADDR_LEB + uleb(site, &mut reloc); + uleb(u64::from(symbol), &mut reloc); + reloc.push(0x00); // addend 0 + } + + let input = build_input_wasm(Some(&reloc)); + + // Sanity: input should parse as a wasm module. + for payload in Parser::new(0).parse_all(&input) { + payload.expect("input wasm is valid"); + } + + let mut tmp = tempfile::tempdir().expect("create tmpdir"); + tmp.disable_cleanup(true); + let main_out = tmp.path().join("main.wasm"); + + let mut opts = Options::new(&input); + opts.output_dir = tmp.path(); + opts.main_out_path = &main_out; + + // --- Assertion 1: the undefined data symbol must not be fatal. + transform(opts).expect("transform succeeds despite the undefined data symbol"); + + let main_bytes = std::fs::read(&main_out).expect("read main.wasm output"); + let (_, reads_data_idx) = locate_export(&main_bytes, READS_DATA_EXPORT); + let immediates = i32_const_immediates(&nth_defined_body(&main_bytes, reads_data_idx)); + let [defined_imm, undefined_imm] = immediates[..] else { + panic!("expected two i32.const sites in reads_data, got {immediates:?}"); + }; + + // --- Assertion 2: the defined symbol's site was relocated to the + // output segment. This is the canary that the reloc.CODE section in + // this test really attaches to the dependency pass: without it, the + // output would carry no data segment at all and assertion 1 would + // pass vacuously. + let segment_base = single_segment_base(&main_bytes); + assert_eq!( + defined_imm, segment_base, + "the defined data symbol's reference should point at the output \ + segment base {segment_base}, got {defined_imm}", + ); + + // --- Assertion 3: the undefined symbol's site keeps the address the + // linker resolved. There is no definition whose relocation could + // change it. + assert_eq!( + undefined_imm, 0, + "the undefined data symbol's reference must stay at the \ + linker-resolved address 0, got {undefined_imm}", + ); + + tmp.disable_cleanup(false); +} From 01480de75727a7bc26007e1c08ce8c88086a04fa Mon Sep 17 00:00:00 2001 From: Sylvain Vieujot Date: Tue, 1 Sep 2026 20:14:50 +0400 Subject: [PATCH 3/6] cite the rustc issue behind undefined data symbols The producer of these symbols is tracked as rust-lang/rust#81280: incremental builds - particularly interrupted ones - reuse objects that reference promoted anonymous globals whose content-derived names changed. Name the issue and the remediation (a clean rebuild of the defining crate) in both the code comment and the warning, so the tolerance does not hide the compiler bug from anyone who hits it. --- crates/wasm_split_cli/src/emit.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/wasm_split_cli/src/emit.rs b/crates/wasm_split_cli/src/emit.rs index a5448b3..0149572 100644 --- a/crates/wasm_split_cli/src/emit.rs +++ b/crates/wasm_split_cli/src/emit.rs @@ -375,15 +375,26 @@ impl DataEmitInfo { } = input_module.reloc_info.symbols[symbol_index] else { // An undefined data symbol carries no definition to - // place or relocate. Linkers accept these under - // --allow-undefined (rustc incremental builds can - // leave references to promoted anonymous globals - // whose content-derived names changed), so keep the - // references exactly as the linker resolved them. + // place or relocate. The usual producer is + // rust-lang/rust#81280: an incremental build - + // particularly an interrupted one - reuses an object + // that still references a promoted anonymous global + // (`anon...llvm.`) whose content-derived + // name changed in a recompiled codegen unit. Native + // linkers reject that shape, but wasm-ld accepts it + // under --allow-undefined and resolves the references + // to address 0 - so keep them exactly as the linker + // resolved them rather than failing on a module the + // linker produced. if let SymbolInfo::Data { name, .. } = input_module.reloc_info.symbols[symbol_index] { - warn!("Undefined data symbol {name:?} in included set; leaving its references unchanged."); + warn!( + "Undefined data symbol {name:?} in included set; leaving its \ + references unchanged. This usually comes from an interrupted \ + incremental build (rust-lang/rust#81280); a clean rebuild of \ + the defining crate makes it go away." + ); } return None; }; From ddb5c2659d29ee8b9848f3041781621a6be03e3c Mon Sep 17 00:00:00 2001 From: Sylvain Vieujot Date: Wed, 2 Sep 2026 18:46:20 +0400 Subject: [PATCH 4/6] treat undefined data symbols as unrelocatable rather than special An undefined data symbol has no definition, so there is nothing to place and no address to relocate; the linker already resolved its references (to 0 under --allow-undefined) and reloc_value leaves such references untouched. Dropping the symbol from the emit state is the general rule, not a workaround for one producer, so the warning that pointed at the rustc incremental-build issue becomes a trace and the comment states the invariant. The hand-built module test goes away in favour of an integration fixture that links a real undefined extern data symbol. --- crates/wasm_split_cli/src/emit.rs | 28 +- .../tests/undefined_data_symbol_end_to_end.rs | 415 ------------------ 2 files changed, 10 insertions(+), 433 deletions(-) delete mode 100644 crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs diff --git a/crates/wasm_split_cli/src/emit.rs b/crates/wasm_split_cli/src/emit.rs index 0149572..a2a11a8 100644 --- a/crates/wasm_split_cli/src/emit.rs +++ b/crates/wasm_split_cli/src/emit.rs @@ -374,27 +374,19 @@ impl DataEmitInfo { .. } = input_module.reloc_info.symbols[symbol_index] else { - // An undefined data symbol carries no definition to - // place or relocate. The usual producer is - // rust-lang/rust#81280: an incremental build - - // particularly an interrupted one - reuses an object - // that still references a promoted anonymous global - // (`anon...llvm.`) whose content-derived - // name changed in a recompiled codegen unit. Native - // linkers reject that shape, but wasm-ld accepts it - // under --allow-undefined and resolves the references - // to address 0 - so keep them exactly as the linker - // resolved them rather than failing on a module the - // linker produced. + // Undefined data symbol: nothing defines it, so there is + // no definition to place and no address to relocate. + // The linker already resolved every reference to it + // (to 0 under --allow-undefined), and `reloc_value` + // leaves such references untouched, so the symbol + // simply has no place in the emit state. Toolchains + // produce these in the wild, e.g. rustc incremental + // builds whose reused objects still reference renamed + // promoted anonymous globals (rust-lang/rust#81280). if let SymbolInfo::Data { name, .. } = input_module.reloc_info.symbols[symbol_index] { - warn!( - "Undefined data symbol {name:?} in included set; leaving its \ - references unchanged. This usually comes from an interrupted \ - incremental build (rust-lang/rust#81280); a clean rebuild of \ - the defining crate makes it go away." - ); + trace!("undefined data symbol {name:?} in included set; references keep their linker value"); } return None; }; diff --git a/crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs b/crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs deleted file mode 100644 index e925732..0000000 --- a/crates/wasm_split_cli/tests/undefined_data_symbol_end_to_end.rs +++ /dev/null @@ -1,415 +0,0 @@ -//! Builds a minimal splittable wasm whose symbol table carries an -//! *undefined* data symbol - `definition: None`, `WASM_SYM_UNDEFINED` - -//! that is referenced by a `R_WASM_MEMORY_ADDR_LEB` relocation from a -//! function kept in the main module. rustc produces this shape with -//! `incremental` + multiple codegen units: a reused object can keep -//! references to a promoted anonymous global (`anon...llvm.`) -//! whose content-derived name changed in a recompiled unit, and wasm-ld -//! links the dangling reference under `--allow-undefined`, resolving it -//! to address 0. -//! -//! Runs the full `wasm_split_cli_support::transform()` pipeline on it and -//! asserts: -//! -//! 1. the transform succeeds - the undefined symbol has no definition -//! to place or relocate, so it must be skipped, not treated as -//! fatal; and -//! 2. the reference to the *defined* data symbol next to it is still -//! relocated to the output segment's base - proving the relocation -//! plumbing in this test attaches to the dependency pass (a broken -//! reloc encoding would make assertion 1 pass vacuously); and -//! 3. the reference to the undefined symbol keeps the address the -//! linker resolved (0), untouched by any fixup. -//! -//! See #59. - -use std::borrow::Cow; - -use wasm_encoder::{ - CodeSection, ConstExpr, CustomSection, DataSection, DataSymbolDefinition, Encode, ExportKind, - ExportSection, Function, FunctionSection, ImportSection, Instruction, MemorySection, - MemoryType, Module, RefType, SymbolTable, TableSection, TableType, TypeSection, -}; -use wasmparser::{DataKind, Operator, Parser, Payload, TypeRef}; - -use wasm_split_cli_support::{transform, Options}; - -#[path = "../src/magic_constants.rs"] -mod magic_constants; - -/// Input layout (with 1 import): 0 = placeholder import, 1 = split_body, -/// 2 = main, 3 = reads_data. -const SPLIT_BODY_IDX: u32 = 1; -const MAIN_IDX: u32 = SPLIT_BODY_IDX + 1; -const READS_DATA_IDX: u32 = MAIN_IDX + 1; -const READS_DATA_EXPORT: &str = "reads_data"; - -/// Symbol table layout. -const DEFINED_DATA_SYM: u32 = 1; -const UNDEFINED_DATA_SYM: u32 = 2; - -const SEGMENT_BASE: i32 = 1024; -const SEGMENT_BYTES: &[u8] = b"0123456789abcdef"; - -/// `i32.const` with a linker-style 5-byte padded LEB immediate, the shape -/// relocation fixups patch in place. -fn padded_i32_const(value: u32) -> [u8; 6] { - let mut bytes = [0x41, 0x80, 0x80, 0x80, 0x80, 0x00]; - bytes[1] |= (value & 0x7f) as u8; - bytes[2] |= ((value >> 7) & 0x7f) as u8; - bytes[3] |= ((value >> 14) & 0x7f) as u8; - bytes[4] |= ((value >> 21) & 0x7f) as u8; - bytes[5] = ((value >> 28) & 0x0f) as u8; - bytes -} - -fn uleb(mut value: u64, out: &mut Vec) { - loop { - let byte = (value & 0x7f) as u8; - value >>= 7; - if value == 0 { - out.push(byte); - return; - } - out.push(byte | 0x80); - } -} - -/// The module, optionally with a `reloc.CODE` custom section appended. -/// Two-pass construction: the first pass (without relocations) is byte -/// identical up to the appended section, so offsets measured on it stay -/// valid for the second. -fn build_input_wasm(reloc_code: Option<&[u8]>) -> Vec { - let mut module = Module::new(); - - let mut types = TypeSection::new(); - types.ty().function([], []); - module.section(&types); - - const SPLIT_NAME: &str = "testsplit"; - const SPLIT_HASH: &str = "00000000000000000000000000000000"; - let import_name = format!("__wasm_split_00{SPLIT_NAME}00_import_{SPLIT_HASH}"); - let export_name = format!("__wasm_split_00{SPLIT_NAME}00_export_{SPLIT_HASH}"); - - let mut imports = ImportSection::new(); - imports.import( - magic_constants::PLACEHOLDER_IMPORT_MODULE, - &import_name, - wasm_encoder::EntityType::Function(0), - ); - module.section(&imports); - - let mut functions = FunctionSection::new(); - for _ in 1..=READS_DATA_IDX { - functions.function(0); - } - module.section(&functions); - - let mut tables = TableSection::new(); - tables.table(TableType { - element_type: RefType::FUNCREF, - minimum: 1, - maximum: Some(1), - table64: false, - shared: false, - }); - module.section(&tables); - - let mut memories = MemorySection::new(); - memories.memory(MemoryType { - minimum: 1, - maximum: None, - memory64: false, - shared: false, - page_size_log2: None, - }); - module.section(&memories); - - let mut exports = ExportSection::new(); - exports.export(&export_name, ExportKind::Func, SPLIT_BODY_IDX); - exports.export("main", ExportKind::Func, MAIN_IDX); - exports.export(READS_DATA_EXPORT, ExportKind::Func, READS_DATA_IDX); - exports.export("__indirect_function_table", ExportKind::Table, 0); - exports.export("memory", ExportKind::Memory, 0); - module.section(&exports); - - let mut code = CodeSection::new(); - - let mut split_body = Function::new([]); - split_body.instruction(&Instruction::End); - code.function(&split_body); - - let mut main = Function::new([]); - main.instruction(&Instruction::End); - code.function(&main); - - // reads_data: two padded-LEB `i32.const` sites, one per relocation. - // Raw bytes rather than the instruction API: the immediates must be - // 5-byte padded LEBs for an in-place fixup, and the encoder emits - // minimal ones. - let mut reads_data = vec![0x00]; // no locals - reads_data.extend_from_slice(&padded_i32_const(SEGMENT_BASE as u32)); - reads_data.push(0x1a); // drop - reads_data.extend_from_slice(&padded_i32_const(0)); - reads_data.push(0x1a); // drop - reads_data.push(0x0b); // end - code.raw(&reads_data); - - module.section(&code); - - let mut data = DataSection::new(); - data.active( - 0, - &ConstExpr::i32_const(SEGMENT_BASE), - SEGMENT_BYTES.iter().copied(), - ); - module.section(&data); - - let mut sym_tab = SymbolTable::new(); - sym_tab.table( - SymbolTable::WASM_SYM_BINDING_LOCAL, - 0, - Some("__indirect_function_table"), - ); - // Symbol 1: an ordinary defined data symbol covering the segment. - sym_tab.data( - 0, - "defined_data", - Some(DataSymbolDefinition { - index: 0, - offset: 0, - size: SEGMENT_BYTES.len() as u32, - }), - ); - // Symbol 2: the undefined data symbol - no definition part. The name - // mirrors what rustc incremental builds leave behind. - sym_tab.data( - SymbolTable::WASM_SYM_UNDEFINED, - "anon.baadf00dcafe.7.llvm.1234567890", - None, - ); - // Hand-built "linking" custom section: wasm-encoder's LinkingSection - // has no segment-info API yet, and the reader requires one entry per - // data segment. Layout: version, WASM_SEGMENT_INFO subsection, then - // the encoded symbol table subsection. - let mut linking_data = Vec::new(); - uleb(2, &mut linking_data); // linking metadata version - let mut segment_info = Vec::new(); - uleb(1, &mut segment_info); // one segment - uleb(".data".len() as u64, &mut segment_info); - segment_info.extend_from_slice(b".data"); - uleb(0, &mut segment_info); // alignment (log2) - uleb(0, &mut segment_info); // flags - linking_data.push(0x05); // WASM_SEGMENT_INFO - uleb(segment_info.len() as u64, &mut linking_data); - linking_data.extend_from_slice(&segment_info); - sym_tab.encode(&mut linking_data); - module.section(&CustomSection { - name: Cow::Borrowed("linking"), - data: Cow::Borrowed(&linking_data), - }); - - // wasm-split marker: [tag u8][payload_len uleb][payload]. (see wasm_split/../marker.rs) - let ws_payload = [1u8, 1u8, 1u8]; - module.section(&CustomSection { - name: Cow::Borrowed(magic_constants::LINK_SECTION), - data: Cow::Borrowed(&ws_payload), - }); - - if let Some(payload) = reloc_code { - module.section(&CustomSection { - name: Cow::Borrowed("reloc.CODE"), - data: Cow::Borrowed(payload), - }); - } - - module.finish() -} - -/// `(section_index, contents_range)` of the code section, counting section -/// indices the way the relocation reader does: one per section payload, in -/// module order. -fn locate_code_section(wasm: &[u8]) -> (u64, std::ops::Range) { - let mut section_index: u64 = 0; - for payload in Parser::new(0).parse_all(wasm) { - let payload = payload.expect("valid wasm"); - if let Payload::CodeSectionStart { range, .. } = &payload { - return (section_index, range.start as u64..range.end as u64); - } - if payload.as_section().is_some() { - section_index += 1; - } - } - panic!("module has no code section"); -} - -/// Byte offset of `pattern` in `haystack`; the pattern must be unique. -fn locate_unique(haystack: &[u8], pattern: &[u8]) -> u64 { - let mut positions = haystack - .windows(pattern.len()) - .enumerate() - .filter(|(_, w)| *w == pattern) - .map(|(i, _)| i as u64); - let position = positions.next().expect("pattern not found"); - assert!(positions.next().is_none(), "pattern is not unique"); - position -} - -/// `(func_export_index, defined_func_index)`. -fn locate_export(wasm: &[u8], export_name: &str) -> (u32, usize) { - let mut import_count: usize = 0; - let mut target: Option = None; - for payload in Parser::new(0).parse_all(wasm) { - match payload.expect("valid wasm") { - Payload::ImportSection(reader) => { - for imp in reader { - let Ok(wasmparser::Imports::Single(_, imp)) = imp else { - panic!("valid import") - }; - if matches!(imp.ty, TypeRef::Func(_)) { - import_count += 1; - } - } - } - Payload::ExportSection(reader) => { - for exp in reader { - let exp = exp.expect("valid export"); - if exp.name == export_name { - target = Some(exp.index); - break; - } - } - } - _ => {} - } - } - let idx = target.unwrap_or_else(|| panic!("export {export_name:?} not found")); - let defined = (idx as usize).checked_sub(import_count).unwrap_or_else(|| { - panic!( - "export {export_name:?} points at imported index {idx}, \ - not a defined function (imports = {import_count})" - ) - }); - (idx, defined) -} - -fn nth_defined_body<'a>(wasm: &'a [u8], nth: usize) -> wasmparser::FunctionBody<'a> { - Parser::new(0) - .parse_all(wasm) - .filter_map(|p| match p.expect("valid wasm") { - Payload::CodeSectionEntry(body) => Some(body), - _ => None, - }) - .nth(nth) - .unwrap_or_else(|| panic!("module has fewer than {} defined functions", nth + 1)) -} - -/// The immediates of every `i32.const` in a body, in order. -fn i32_const_immediates(body: &wasmparser::FunctionBody<'_>) -> Vec { - let mut reader = body.get_operators_reader().expect("operators"); - let mut values = Vec::new(); - while !reader.eof() { - if let Operator::I32Const { value } = reader.read().expect("op") { - values.push(value); - } - } - values -} - -/// Base address of the module's single active data segment. -fn single_segment_base(wasm: &[u8]) -> i32 { - let mut base: Option = None; - for payload in Parser::new(0).parse_all(wasm) { - if let Payload::DataSection(reader) = payload.expect("valid wasm") { - for segment in reader { - let segment = segment.expect("valid data segment"); - let DataKind::Active { offset_expr, .. } = segment.kind else { - panic!("expected an active data segment"); - }; - let mut ops = offset_expr.get_operators_reader(); - let Operator::I32Const { value } = ops.read().expect("op") else { - panic!("expected an i32.const segment offset"); - }; - assert!(base.is_none(), "expected exactly one data segment"); - base = Some(value); - } - } - } - base.expect("output module has no data segment - did the relocations attach?") -} - -#[test] -fn undefined_data_symbol_is_skipped_not_fatal() { - tracing_subscriber::fmt::init(); - - // Pass 1: measure the relocation targets on a module without the - // reloc section (appending it does not shift earlier offsets). - let unrelocated = build_input_wasm(None); - let (code_section_index, code_range) = locate_code_section(&unrelocated); - let defined_site = - locate_unique(&unrelocated, &padded_i32_const(SEGMENT_BASE as u32)) + 1 - code_range.start; - let undefined_site = locate_unique(&unrelocated, &padded_i32_const(0)) + 1 - code_range.start; - - // R_WASM_MEMORY_ADDR_LEB entries against the defined and the - // undefined data symbol. - let mut reloc = Vec::new(); - uleb(code_section_index, &mut reloc); - uleb(2, &mut reloc); - for (site, symbol) in [ - (defined_site, DEFINED_DATA_SYM), - (undefined_site, UNDEFINED_DATA_SYM), - ] { - reloc.push(0x03); // R_WASM_MEMORY_ADDR_LEB - uleb(site, &mut reloc); - uleb(u64::from(symbol), &mut reloc); - reloc.push(0x00); // addend 0 - } - - let input = build_input_wasm(Some(&reloc)); - - // Sanity: input should parse as a wasm module. - for payload in Parser::new(0).parse_all(&input) { - payload.expect("input wasm is valid"); - } - - let mut tmp = tempfile::tempdir().expect("create tmpdir"); - tmp.disable_cleanup(true); - let main_out = tmp.path().join("main.wasm"); - - let mut opts = Options::new(&input); - opts.output_dir = tmp.path(); - opts.main_out_path = &main_out; - - // --- Assertion 1: the undefined data symbol must not be fatal. - transform(opts).expect("transform succeeds despite the undefined data symbol"); - - let main_bytes = std::fs::read(&main_out).expect("read main.wasm output"); - let (_, reads_data_idx) = locate_export(&main_bytes, READS_DATA_EXPORT); - let immediates = i32_const_immediates(&nth_defined_body(&main_bytes, reads_data_idx)); - let [defined_imm, undefined_imm] = immediates[..] else { - panic!("expected two i32.const sites in reads_data, got {immediates:?}"); - }; - - // --- Assertion 2: the defined symbol's site was relocated to the - // output segment. This is the canary that the reloc.CODE section in - // this test really attaches to the dependency pass: without it, the - // output would carry no data segment at all and assertion 1 would - // pass vacuously. - let segment_base = single_segment_base(&main_bytes); - assert_eq!( - defined_imm, segment_base, - "the defined data symbol's reference should point at the output \ - segment base {segment_base}, got {defined_imm}", - ); - - // --- Assertion 3: the undefined symbol's site keeps the address the - // linker resolved. There is no definition whose relocation could - // change it. - assert_eq!( - undefined_imm, 0, - "the undefined data symbol's reference must stay at the \ - linker-resolved address 0, got {undefined_imm}", - ); - - tmp.disable_cleanup(false); -} From b6190203619e5c680e7cc7bf7ca0789218c5973f Mon Sep 17 00:00:00 2001 From: Sylvain Vieujot Date: Wed, 2 Sep 2026 18:46:49 +0400 Subject: [PATCH 5/6] add an integration fixture with an undefined extern data symbol The crate references an extern data symbol that nothing defines and links under --allow-undefined (re-added by its build script, since rustc no longer passes the flag - rust-lang/rust#149868), so wasm-ld keeps the symbol undefined in the symbol table, resolves the reference to address 0 and leaves a relocation against it. The split must accept the module and the reference must keep the linker's address; the fixture also carries a split point so the whole pipeline runs. This is the shape rustc incremental builds leave behind under rust-lang/rust#81280. Before the previous commit the split failed with 'Expected data symbol dep node to ref to defined data symbol'. --- integration/Cargo.lock | 9 +++++ integration/Cargo.toml | 1 + integration/undefined_data/Cargo.toml | 15 ++++++++ integration/undefined_data/build.rs | 9 +++++ integration/undefined_data/src/lib.rs | 55 +++++++++++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 integration/undefined_data/Cargo.toml create mode 100644 integration/undefined_data/build.rs create mode 100644 integration/undefined_data/src/lib.rs diff --git a/integration/Cargo.lock b/integration/Cargo.lock index 7d56b9b..0fc91af 100644 --- a/integration/Cargo.lock +++ b/integration/Cargo.lock @@ -416,6 +416,15 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "undefined_data" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", + "wasm-bindgen-test", + "wasm_split_helpers", +] + [[package]] name = "unicode-ident" version = "1.0.19" diff --git a/integration/Cargo.toml b/integration/Cargo.toml index 4993d2d..b0adf92 100644 --- a/integration/Cargo.toml +++ b/integration/Cargo.toml @@ -8,6 +8,7 @@ members = [ "callback-in-split", "optimize_splits", "static_mut", + "undefined_data", ] resolver = "2" diff --git a/integration/undefined_data/Cargo.toml b/integration/undefined_data/Cargo.toml new file mode 100644 index 0000000..2f5727e --- /dev/null +++ b/integration/undefined_data/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "undefined_data" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen.workspace = true +wasm_split_helpers.workspace = true + +[dev-dependencies] +wasm-bindgen-test.workspace = true diff --git a/integration/undefined_data/build.rs b/integration/undefined_data/build.rs new file mode 100644 index 0000000..9974a78 --- /dev/null +++ b/integration/undefined_data/build.rs @@ -0,0 +1,9 @@ +fn main() { + // rustc stopped passing `--allow-undefined` to wasm-ld + // (rust-lang/rust#149868). Workflows that still pass it themselves get + // modules in which an undefined data symbol links to address 0 instead + // of failing the link; this fixture opts in to produce that shape. + if std::env::var("CARGO_CFG_TARGET_FAMILY").as_deref() == Ok("wasm") { + println!("cargo:rustc-link-arg=--allow-undefined"); + } +} diff --git a/integration/undefined_data/src/lib.rs b/integration/undefined_data/src/lib.rs new file mode 100644 index 0000000..917fa5a --- /dev/null +++ b/integration/undefined_data/src/lib.rs @@ -0,0 +1,55 @@ +//! The main module references a data symbol that nothing defines. Linked +//! under `--allow-undefined`, wasm-ld keeps the symbol in the symbol table +//! as undefined, resolves every reference to address 0, and leaves a +//! `R_WASM_MEMORY_ADDR_*` relocation against it. The splitter must accept +//! such a module: an undefined data symbol has no definition to place or +//! relocate, so its references keep the linker's value. +//! +//! rustc incremental builds produce this shape in the wild when a reused +//! object still references a promoted anonymous global whose name changed +//! (rust-lang/rust#81280). +//! +//! Wasm only: a native linker rejects the undefined reference outright. +#![cfg(target_family = "wasm")] + +use wasm_split_helpers::wasm_split; + +extern "C" { + static UNDEFINED_DATA: u8; +} + +/// The address the linker resolved for the undefined data symbol. +pub fn undefined_data_address() -> usize { + // Taking the address, not reading through it, keeps the relocation + // against the undefined symbol without touching address 0. + unsafe { core::ptr::addr_of!(UNDEFINED_DATA) as usize } +} + +#[wasm_split(split)] +fn lazy() -> u32 { + 42 +} + +#[cfg(test)] +mod tests { + use wasm_bindgen_test::wasm_bindgen_test as test; + + // This pulls in the magic marker which we assert in all tests + const _: () = { + let _ = wasm_split_helpers::rt::ensure_loaded; + }; + + #[test] + fn undefined_data_keeps_the_linker_address() { + assert_eq!( + super::undefined_data_address(), + 0, + "an undefined data symbol resolves to address 0 and must not be relocated" + ); + } + + #[test] + async fn split_still_loads() { + assert_eq!(super::lazy().await, 42); + } +} From a719b1a96b259818086e61dd3969c96a3e7d1b26 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Mon, 7 Sep 2026 13:35:19 +0200 Subject: [PATCH 6/6] change test to weak symbols --- integration/Cargo.lock | 6 ++-- integration/Cargo.toml | 2 ++ integration/undefined_data/Cargo.toml | 4 +++ integration/undefined_data/build.rs | 34 ++++++++++++++++---- integration/undefined_data/src/lib.rs | 45 +++++++++++++++------------ 5 files changed, 63 insertions(+), 28 deletions(-) diff --git a/integration/Cargo.lock b/integration/Cargo.lock index 0fc91af..b81a249 100644 --- a/integration/Cargo.lock +++ b/integration/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base16" @@ -420,6 +420,8 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" name = "undefined_data" version = "0.1.0" dependencies = [ + "autocfg", + "rustversion", "wasm-bindgen", "wasm-bindgen-test", "wasm_split_helpers", diff --git a/integration/Cargo.toml b/integration/Cargo.toml index b0adf92..313fbb5 100644 --- a/integration/Cargo.toml +++ b/integration/Cargo.toml @@ -23,3 +23,5 @@ wasm-bindgen = "=0.2.127" wasm-bindgen-futures = "0.4.55" wasm-bindgen-test = "0.3.55" tokio = { version = "1.48", features = ["rt", "macros"] } +autocfg = "1" +rustversion = "1" diff --git a/integration/undefined_data/Cargo.toml b/integration/undefined_data/Cargo.toml index 2f5727e..9a49d99 100644 --- a/integration/undefined_data/Cargo.toml +++ b/integration/undefined_data/Cargo.toml @@ -13,3 +13,7 @@ wasm_split_helpers.workspace = true [dev-dependencies] wasm-bindgen-test.workspace = true + +[build-dependencies] +autocfg.workspace = true +rustversion.workspace = true diff --git a/integration/undefined_data/build.rs b/integration/undefined_data/build.rs index 9974a78..f3f5665 100644 --- a/integration/undefined_data/build.rs +++ b/integration/undefined_data/build.rs @@ -1,9 +1,31 @@ +const WEAK_LINKAGE_PROBE: &str = r#" +extern "C" { + #[linkage = "extern_weak"] + static _OPTIONAL_VARIABLE: Option<&'static i32>; +} +"#; + fn main() { - // rustc stopped passing `--allow-undefined` to wasm-ld - // (rust-lang/rust#149868). Workflows that still pass it themselves get - // modules in which an undefined data symbol links to address 0 instead - // of failing the link; this fixture opts in to produce that shape. - if std::env::var("CARGO_CFG_TARGET_FAMILY").as_deref() == Ok("wasm") { - println!("cargo:rustc-link-arg=--allow-undefined"); + println!("cargo:rerun-if-changed=build.rs"); + + let is_nightly = rustversion::cfg!(nightly); + let ac = autocfg::new(); + let has_weak_linkage = ac.probe_raw(WEAK_LINKAGE_PROBE).is_ok(); + let has_weak_linkage_needs_feature = !has_weak_linkage + && ac + .probe_raw(&format!("#![feature(linkage)]\n{WEAK_LINKAGE_PROBE}")) + .is_ok(); + assert!( + !is_nightly || (has_weak_linkage || has_weak_linkage_needs_feature), + "expected weak linkage to work on nightly" + ); + + autocfg::emit_possibility("has_weak_linkage"); + autocfg::emit_possibility("has_weak_linkage_needs_feature"); + if has_weak_linkage || has_weak_linkage_needs_feature { + autocfg::emit("has_weak_linkage"); + } + if has_weak_linkage_needs_feature { + autocfg::emit("has_weak_linkage_needs_feature"); } } diff --git a/integration/undefined_data/src/lib.rs b/integration/undefined_data/src/lib.rs index 917fa5a..b8db2ec 100644 --- a/integration/undefined_data/src/lib.rs +++ b/integration/undefined_data/src/lib.rs @@ -1,28 +1,32 @@ -//! The main module references a data symbol that nothing defines. Linked -//! under `--allow-undefined`, wasm-ld keeps the symbol in the symbol table -//! as undefined, resolves every reference to address 0, and leaves a +//! The main module references a data symbol with weak linkage. This will +//! resolves every reference to address 0, but will still leaves a //! `R_WASM_MEMORY_ADDR_*` relocation against it. The splitter must accept -//! such a module: an undefined data symbol has no definition to place or -//! relocate, so its references keep the linker's value. +//! such a module. //! -//! rustc incremental builds produce this shape in the wild when a reused -//! object still references a promoted anonymous global whose name changed -//! (rust-lang/rust#81280). -//! -//! Wasm only: a native linker rejects the undefined reference outright. -#![cfg(target_family = "wasm")] +//! This is to mock a bug (rust-lang/rust#81280) occuring in the wild. In +//! this case, rustc incremental builds produce a similar shape. Here, an +//! undefined reference to a promoted anonymous global whose name changed +//! gets resolves to address 0 with a linker arg, `--allow-undefined`, +//! passed to wasm-ld as a workaround. +#![cfg_attr(has_weak_linkage_needs_feature, feature(linkage))] use wasm_split_helpers::wasm_split; -extern "C" { - static UNDEFINED_DATA: u8; -} +#[cfg(has_weak_linkage)] +pub mod weak { + unsafe extern "C" { + #[linkage = "extern_weak"] + safe static UNDEFINED_DATA: Option<&'static i32>; + } -/// The address the linker resolved for the undefined data symbol. -pub fn undefined_data_address() -> usize { - // Taking the address, not reading through it, keeps the relocation - // against the undefined symbol without touching address 0. - unsafe { core::ptr::addr_of!(UNDEFINED_DATA) as usize } + /// The address the linker resolved for the undefined data symbol. + pub fn undefined_data_address() -> usize { + // Taking the address, not reading through it, keeps the relocation + // against the undefined symbol without touching address 0. + UNDEFINED_DATA + .map(|data| (data as *const i32).addr()) + .unwrap_or(0) + } } #[wasm_split(split)] @@ -39,10 +43,11 @@ mod tests { let _ = wasm_split_helpers::rt::ensure_loaded; }; + #[cfg(has_weak_linkage)] #[test] fn undefined_data_keeps_the_linker_address() { assert_eq!( - super::undefined_data_address(), + super::weak::undefined_data_address(), 0, "an undefined data symbol resolves to address 0 and must not be relocated" );