Skip to content

Commit 6fa7d42

Browse files
alexmalyshevmeta-codesync[bot]
authored andcommitted
Drop unreachable blocks after inlining
Summary: The HIR inliner could crash when inlining a callee that has no reachable `return`, for example a `while True:` loop whose only exit is an exception. `HIRBuilder::inlineHIR` builds the callee CFG but, unlike the normal `buildHIR` path, does not run `removeUnreachableBlocks`. When the callee has no reachable return, the merged exit block (and the surrounding return-value `Assign`/`Branch` blocks produced by `inlineFunctionCall`) end up with no predecessors and are left in the caller's CFG. `InlineFunctionCalls::Run` then ran `CopyPropagation` and `CleanCFG` over those stale blocks; `CleanCFG` -> `PhiElimination` -> `chaseAssignOperand` dereferenced instructions that had already been freed during inlining, segfaulting inside `InlineFunctionCalls::Run`. Fixes #120. Reviewed By: mpage Differential Revision: D110056992 fbshipit-source-id: a0c450ad7c414d092f167bec2e1de4ea039519f4
1 parent 194b430 commit 6fa7d42

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

cinderx/Jit/hir/inliner.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ void InlineFunctionCalls::Run(Function& irfunc) {
608608
// unreachable and therefore make less work (less to inline), we cannot remove
609609
// unreachable blocks in the above loop. It might delete instructions pointed
610610
// to by `calls`.
611+
removeUnreachableBlocks(irfunc);
611612
CopyPropagation{}.Run(irfunc);
612613
CleanCFG{}.Run(irfunc);
613614
}

cinderx/PythonLib/test_cinderx/test_jit_inliner.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,74 @@ def caller_of_warm_branch(x: int, take_cold: bool) -> int:
190190
return warm_branch(x, take_cold) + 1
191191

192192

193+
# Regression model for a JIT inliner crash. `flag_set_loop` is a `while True:` loop
194+
# whose only exit is the KeyError raised by the lookup. There is no reachable `return`
195+
# inside the loop so when it is inlined its merged exit block has no predecessors and is
196+
# unreachable. The inliner used to run CopyPropagation and CleanCFG over that stale
197+
# block (left behind because it can't be removed during the inlining loop), which
198+
# dereferenced freed instructions and crashed.
199+
_FLAG_VALUES = {"a": 1, "b": 2, "i": 4, "L": 8}
200+
_LOCALE = 8
201+
_GLOBAL = 16
202+
203+
204+
class FlagSource:
205+
def __init__(self, chars: str) -> None:
206+
self._chars = chars
207+
self.pos = 0
208+
209+
def get(self) -> str:
210+
ch = self._chars[self.pos]
211+
self.pos += 1
212+
return ch
213+
214+
def match(self, ch: str) -> bool:
215+
if self.pos < len(self._chars) and self._chars[self.pos] == ch:
216+
self.pos += 1
217+
return True
218+
return False
219+
220+
221+
class FlagInfo:
222+
inline_locale: bool = False
223+
224+
225+
def flag_set_loop(source: FlagSource) -> int:
226+
flags = 0
227+
saved_pos = 0
228+
try:
229+
while True:
230+
saved_pos = source.pos
231+
ch = source.get()
232+
flags |= _FLAG_VALUES[ch]
233+
except KeyError:
234+
source.pos = saved_pos
235+
return flags
236+
237+
238+
def parse_flags(source: FlagSource, info: FlagInfo) -> tuple[int, int]:
239+
flags_on = flag_set_loop(source)
240+
if source.match("-"):
241+
flags_off = flag_set_loop(source)
242+
if not flags_off:
243+
raise ValueError("no flags after '-'")
244+
else:
245+
flags_off = 0
246+
if flags_on & _LOCALE:
247+
info.inline_locale = True
248+
return flags_on, flags_off
249+
250+
251+
def parse_flags_subpattern(source: FlagSource, info: FlagInfo) -> tuple[int, int]:
252+
flags_on, flags_off = parse_flags(source, info)
253+
if flags_off & _GLOBAL:
254+
raise ValueError("cannot turn off global flag")
255+
if flags_on & flags_off:
256+
raise ValueError("flag turned on and off")
257+
flags_on &= ~_GLOBAL
258+
return flags_on, flags_off
259+
260+
193261
@passUnless(INLINER, "Testing the inliner")
194262
class InlinedFunctionTests(unittest.TestCase):
195263
@jit_suppress
@@ -365,6 +433,27 @@ def test_inline_function_stats(self) -> None:
365433
next(iter(has_varargs)),
366434
)
367435

436+
@jit_suppress
437+
def test_inlining_callee_without_reachable_return(self) -> None:
438+
"""Inlining a function whose only loop exit is an exception (so it has
439+
no reachable return) leaves an unreachable exit block in the caller.
440+
The inliner must drop that block before running CopyPropagation/CleanCFG;
441+
otherwise those passes walk freed instructions and crash."""
442+
cinderx.jit.force_compile(parse_flags_subpattern)
443+
self.assertTrue(cinderx.jit.is_jit_compiled(parse_flags_subpattern))
444+
445+
# parse_flags is inlined, and flag_set_loop is inlined transitively
446+
# through it (once for flags_on, and the source has no "-" so flags_off
447+
# stays a constant), exercising the unreachable-exit-block path.
448+
self.assertGreater(
449+
cinderx.jit.get_num_inlined_functions(parse_flags_subpattern), 1
450+
)
451+
452+
# "abiX" turns on flags a|b|i = 7; the trailing "X" is not a flag, so
453+
# the lookup raises KeyError and ends the loop. There is no "-", so
454+
# flags_off is 0.
455+
self.assertEqual(parse_flags_subpattern(FlagSource("abiX"), FlagInfo()), (7, 0))
456+
368457
@jit_suppress
369458
def test_line_numbers_with_multiple_inlined_calls(self) -> None:
370459
"""Verify that line numbers are correct for inlined calls that appear

0 commit comments

Comments
 (0)