Fix mayNotReturn not checked in invalidates()#8308
Fix mayNotReturn not checked in invalidates()#8308sumleo wants to merge 1 commit intoWebAssembly:mainfrom
Conversation
The invalidates() method in EffectAnalyzer did not consider mayNotReturn when checking if two expressions can be reordered. This allowed optimizations like the select arm swap in OptimizeInstructions to incorrectly reorder a potentially-infinite loop with side-effecting code (e.g., global.set), causing the side effect to always execute when it should have been conditional on the loop terminating. Add mayNotReturn checks against writesGlobalState() in invalidates(). We use writesGlobalState() rather than hasSideEffects() because mayNotReturn only matters for externally observable state changes (globals, memory, tables, structs, arrays, atomics, calls) -- local variable operations are safe to reorder past a may-not-return expression since they are not observable if execution never continues.
96ec337 to
b5743f6
Compare
|
While this is technically true in a way, I don't think we need to change things here. The only way to observe the difference in the example given is if you run the program and pause it in a debugger or some other such tool, when it is infinite-looping. Pausing in a debugger can notice all sorts of other internal changes in the module, for example, reordering two local.sets leads to debugger-observable changes but not user-observable ones. Binaryen only preserves the latter - at least, I've not heard a strong enough reason for us to do anything more, and it has downsides. Concretely, the infinite loop is exactly what prevents userspace from noticing a change to that global. There is simply no opportunity for other code to be influenced. (If the global were shared, an argument could be made that another thread could observe it, but cross-thread update timing is nondeterministic, so I'm not sure even then.) With that said, this is non-obvious behavior, so (1) I'd be open to hearing other opinions, and (2) we should document this better. |
|
Thanks for the detailed explanation. That makes sense — an infinite loop prevents any downstream code from observing the global state change, so the reordering is semantically valid from the perspective of user-observable behavior. I hadn't considered that the non-termination itself acts as the observation barrier. I'll close this PR. Thanks for taking the time to explain the reasoning. |
Summary
EffectAnalyzer::invalidates()checkstransfersControlFlow()to prevent reordering side-effecting expressions, buttransfersControlFlow()does not includemayNotReturn. This means a potentially-infinite loop (which hasmayNotReturn=truebuttransfersControlFlow()=false) can be incorrectly reordered with side-effecting code.Concrete miscompilation:
OptimizeInstructionsflips select arms when the condition isi32.eqz, callingcanReorder()to check safety. With a loop in one arm and aglobal.setin the other,canReorder()returns true (incorrectly), causing theglobal.setto always execute when it should only execute if the loop terminates.Fix: Add
mayNotReturnchecks alongside the existingtransfersControlFlow()checks ininvalidates().Test plan
optimize-instructions-maynotreturn.wastcovering: