Skip to content

Commit 239f931

Browse files
committed
wasm-split-cli: don't zero live, overlapping bytes when pruning dead data symbols
prune_main_symbols() zeroes each unreachable data symbol's own declared byte range in the main module's data segment. LLVM/wasm-ld tail-merges identical *suffixes* of NUL-terminated byte constants to save space - so a short, dead symbol's declared range can be a byte-for-byte suffix of a longer, still-live symbol's range, sharing the same storage. Zeroing the dead symbol's range then silently truncates the live symbol's tail, since the code has no notion that data symbols can overlap. This specifically corrupts core::fmt::Arguments::template - a NUL- terminated bytecode encoding of a format!() call's literal pieces and placeholders (new in recent nightly std, replacing the classic `pieces: &[&str]` slice) - precisely because it's NUL-terminated to make this kind of tail-merging possible. Losing the tail means the core::fmt::write interpreter reads a premature 0x00 and stops right after the first placeholder, so a format!() call's output silently comes out truncated after its first argument. Fixed by computing which bytes are still claimed by a live (reachable) data symbol before zeroing anything, and skipping those bytes specifically when zeroing a dead symbol's range.
1 parent 24f6a82 commit 239f931

1 file changed

Lines changed: 29 additions & 0 deletions

File tree

  • packages/wasm-split/wasm-split-cli/src

packages/wasm-split/wasm-split-cli/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,30 @@ impl<'a> Splitter<'a> {
492492
out.exports.delete(split.export_id);
493493
}
494494

495+
// Data symbols can overlap: LLVM/wasm-ld tail-merges identical
496+
// suffixes of NUL-terminated byte constants (e.g. a short, dead
497+
// `core::fmt::Arguments::template` bytecode blob can be a byte-for-
498+
// byte suffix of a longer, still-live one, sharing storage). Zeroing
499+
// an unused symbol's declared range is only safe where no live
500+
// symbol's range also claims those bytes - so mark every byte a
501+
// live (not-unused) symbol owns before zeroing anything, per data
502+
// segment actually touched below (segment 0 only).
503+
let mut live_bytes: Vec<bool> = out
504+
.data
505+
.iter()
506+
.nth(0)
507+
.map(|data| vec![false; data.value.len()])
508+
.unwrap_or_default();
509+
for (id, symbol) in self.data_symbols.iter() {
510+
if symbol.which_data_segment != 0 || unused_symbols.contains(&Node::DataSymbol(*id)) {
511+
continue;
512+
}
513+
let end = (symbol.segment_offset + symbol.symbol_size).min(live_bytes.len());
514+
for i in symbol.segment_offset.min(end)..end {
515+
live_bytes[i] = true;
516+
}
517+
}
518+
495519
// And then any actual symbols from the callgraph
496520
for symbol in unused_symbols.iter().cloned() {
497521
match symbol {
@@ -517,6 +541,11 @@ impl<'a> Splitter<'a> {
517541
let data_id = out.data.iter().nth(symbol.which_data_segment).unwrap().id();
518542
let data = out.data.get_mut(data_id);
519543
for i in symbol.segment_offset..symbol.segment_offset + symbol.symbol_size {
544+
// Don't stomp bytes a still-live, overlapping
545+
// symbol owns (see the tail-merge comment above).
546+
if live_bytes.get(i).copied().unwrap_or(false) {
547+
continue;
548+
}
520549
data.value[i] = 0;
521550
}
522551
}

0 commit comments

Comments
 (0)