fix(#326): decline the dead-function sweep when a funcref global may hold ref.func - #327
Merged
Merged
Conversation
…hold ref.func Latent silent-miscompile of the #196 class, pre-existing and present in v1.3.0 as shipped. `optimize_fused_module` runs `eliminate_dead_functions` by default on every core module. Two facts combine: * `collect_function_refs_recursive` matches `Instruction::Call` only, so `ref.func` is NOT part of the liveness closure; * the encoder re-emits `global_section_bytes` verbatim -- only numeric globals are lifted into `module.globals`, so a `ref.func N` in a funcref global's init expression is never renumbered. So a function referenced only by a funcref global is swept, the index space is compacted, and the frozen `ref.func N` now designates whichever function inherited index N. Valid wasm, wrong behaviour, invisible to structural validation. That is exactly what #196 shipped through the element section and it cost a flight-control miscompile. The element section was fixed after #196 by remapping it. The identical hazard in the global section was never addressed -- the lesson was applied to the instance, not to the class. Until `ref.func` is a real liveness root AND the global section is remapped (the element-section treatment), decline to sweep such modules. Skipping an optimization costs bytes; getting this wrong costs correctness. Worth recording: the doc comment on `eliminate_dead_functions` already listed "referenced by a `call` or `ref.func` instruction" among the liveness roots. The documentation asserted the property; the code never implemented it, and nothing checked the two against each other. Annotated rather than quietly corrected. Tests: `t326_funcref_global_blocks_the_dead_function_sweep` (the pass declines, no function removed) and `t326_numeric_only_globals_do_not_block_the_sweep` -- the control, without which a guard that always bails would pass the first test. Verified: loom-core --features verification --lib = 483 passed, 0 failed, 2 ignored (baseline 481, +2, none lost). fmt clean. Fixes #326 Refs #196, #239
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Latent silent miscompile of the #196 class — pre-existing, and present in v1.3.0 as shipped.
optimize_fused_modulerunseliminate_dead_functionsby default on every core module.The hazard
Two facts combine:
collect_function_refs_recursivematchesInstruction::Callonly —ref.funcis not in the liveness closure.global_section_bytesverbatim; only numeric globals are lifted intomodule.globals, so aref.func Nin a funcref global's init expression is never renumbered.A function referenced solely by a funcref global is therefore swept, the index space compacts, and the frozen
ref.func Ndesignates whichever function inherited index N. Valid wasm, wrong behaviour, invisible to structural validation.Why this specific shape should have been caught
This is exactly what #196 shipped through the element section, and it cost a flight-control miscompile. The fix then was to remap the element section — which is in place today (
remap_element_section_refs). The identical hazard in the global section was never addressed. The lesson was applied to the instance, not to the class.Also worth recording rather than quietly correcting: the doc comment on
eliminate_dead_functionshas always listed "referenced by acallorref.funcinstruction" among its liveness roots. The documentation asserted the property, the code never implemented it, and nothing compared the two. I annotated it instead of deleting the line, because the drift is the finding.The fix — decline, don't guess
Until
ref.funcis a genuine liveness root and the global section is remapped (the element-section treatment), modules whose raw global section may carryref.funcare not swept. Unparseable section ⇒ treated as hazard-present. Skipping an optimization costs bytes; getting this wrong costs correctness — the charter is explicit about which way that trade goes.Tests
t326_funcref_global_blocks_the_dead_function_sweep— pass declines, no function removed.t326_numeric_only_globals_do_not_block_the_sweep— the control. Without it, a guard that always bails would pass the first test.483 passed / 0 failed / 2 ignored (baseline 481, +2, none lost); fmt clean; full pre-commit suite passed.
Proper fix (
ref.funcas a root + global-section remapping) stays open as the follow-up; this closes the correctness exposure now.Fixes #326
Refs #196, #239, #324