Decompiling to AIL fails because of DirtyStatement
Author: jorik-utwenteCreated May 16, 2025Updated Sep 10, 2026
Labelsbug
Description
I am using angr to decompile Cortex-M firmware to AIL.
For some functions, it fails.
Setup
p = angr.Project(elf_path, auto_load_libs=False)
cfg = p.analyses.CFGFast(normalize=True)
f = cfg.kb.functions.get(f_addr)
ail = p.analyses.Clinic(f)The trace is:
16 f = cfg.kb.functions.get(f_addr)
---> 17 res = p.analyses.Clinic(f)
File ./venv/lib/python3.10/site-packages/angr/analyses/analysis.py:264, in AnalysisFactory.__call__(self, *args, **kwargs)
258 show_progressbar = kwargs.pop("show_progressbar", False)
260 w = self.prep(
261 fail_fast=fail_fast, kb=kb, progress_callback=progress_callback, show_progressbar=show_progressbar
262 )
--> 264 r = w(*args, **kwargs)
265 # clean up so that it's always pickleable
266 r._progressbar = None
File ./venv/lib/python3.10/site-packages/angr/analyses/analysis.py:249, in AnalysisFactory.prep.<locals>.wrapper(*args, **kwargs)
246 oself._progress_callback = progress_callback
248 oself._show_progressbar = show_progressbar
--> 249 oself.__init__(*args, **kwargs)
250 return oself
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/clinic.py:196, in Clinic.__init__(self, func, remove_dead_memdefs, exception_edges, sp_tracker_track_memory, fold_callexprs_into_conditions, insert_labels, optimization_passes, cfg, peephole_optimizations, must_struct, variable_kb, reset_variable_names, rewrite_ites_to_diamonds, cache, mode, sp_shift, inline_functions, inlined_counts, inlining_parents, vvar_id_start, optimization_scratch, desired_variables, force_loop_single_exit, complete_successors, max_type_constraints)
193 self.stack_items[0] = StackItem(0, self.project.arch.bytes, "ret_addr", StackItemType.RET_ADDR)
195 if self._mode == ClinicMode.DECOMPILE:
--> 196 self._analyze_for_decompiling()
197 elif self._mode == ClinicMode.COLLECT_DATA_REFS:
198 self._analyze_for_data_refs()
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/clinic.py:247, in Clinic._analyze_for_decompiling(self)
244 self._max_stack_depth += self.calculate_stack_depth()
245 ail_graph = self._inline_child_functions(ail_graph)
--> 247 ail_graph = self._decompilation_simplifications(ail_graph)
249 if self._desired_variables:
250 ail_graph = self._slice_variables(ail_graph)
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/clinic.py:496, in Clinic._decompilation_simplifications(self, ail_graph)
494 # full-function constant-only propagation
495 self._update_progress(36.0, text="Constant propagation")
--> 496 self._simplify_function(
497 ail_graph,
498 remove_dead_memdefs=False,
499 unify_variables=False,
500 narrow_expressions=False,
501 only_consts=True,
502 fold_callexprs_into_conditions=self._fold_callexprs_into_conditions,
503 max_iterations=1,
504 )
506 # cached block-level reaching definition analysis results and propagator results
507 block_simplification_cache: dict[ailment.Block, NamedTuple] | None = {}
File ./venv/lib/python3.10/site-packages/angr/utils/timing.py:72, in timethis.<locals>.timed_func(*args, **kwargs)
70 _on_func_return(func, start)
71 return r
---> 72 return func(*args, **kwargs)
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/clinic.py:1367, in Clinic._simplify_function(self, ail_graph, remove_dead_memdefs, stack_arg_offsets, unify_variables, max_iterations, narrow_expressions, only_consts, fold_callexprs_into_conditions, rewrite_ccalls, removed_vvar_ids, arg_vvars, preserve_vvar_ids)
1362 """
1363 Simplify the entire function until it reaches a fixed point.
1364 """
1366 for idx in range(max_iterations):
-> 1367 simplified = self._simplify_function_once(
1368 ail_graph,
1369 remove_dead_memdefs=remove_dead_memdefs,
1370 unify_variables=unify_variables,
1371 stack_arg_offsets=stack_arg_offsets,
1372 # only narrow once
1373 narrow_expressions=narrow_expressions and idx == 0,
1374 only_consts=only_consts,
1375 fold_callexprs_into_conditions=fold_callexprs_into_conditions,
1376 rewrite_ccalls=rewrite_ccalls,
1377 removed_vvar_ids=removed_vvar_ids,
1378 arg_vvars=arg_vvars,
1379 preserve_vvar_ids=preserve_vvar_ids,
1380 )
1381 if not simplified:
1382 break
File ./venv/lib/python3.10/site-packages/angr/utils/timing.py:72, in timethis.<locals>.timed_func(*args, **kwargs)
70 _on_func_return(func, start)
71 return r
---> 72 return func(*args, **kwargs)
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/clinic.py:1405, in Clinic._simplify_function_once(self, ail_graph, remove_dead_memdefs, stack_arg_offsets, unify_variables, narrow_expressions, only_consts, fold_callexprs_into_conditions, rewrite_ccalls, removed_vvar_ids, arg_vvars, preserve_vvar_ids)
1384 @timethis
1385 def _simplify_function_once(
1386 self,
(...)
1397 preserve_vvar_ids: set[int] | None = None,
1398 ):
1399 """
1400 Simplify the entire function once.
1401
1402 :return: None
1403 """
-> 1405 simp = self.project.analyses.AILSimplifier(
1406 self.function,
1407 fail_fast=self._fail_fast,
1408 func_graph=ail_graph,
1409 remove_dead_memdefs=remove_dead_memdefs,
1410 unify_variables=unify_variables,
1411 stack_arg_offsets=stack_arg_offsets,
1412 ail_manager=self._ail_manager,
1413 gp=self.function.info.get("gp", None) if self.project.arch.name in {"MIPS32", "MIPS64"} else None,
1414 narrow_expressions=narrow_expressions,
1415 only_consts=only_consts,
1416 fold_callexprs_into_conditions=fold_callexprs_into_conditions,
1417 use_callee_saved_regs_at_return=not self._register_save_areas_removed,
1418 rewrite_ccalls=rewrite_ccalls,
1419 removed_vvar_ids=removed_vvar_ids,
1420 arg_vvars=arg_vvars,
1421 secondary_stackvars=self.secondary_stackvars,
1422 avoid_vvar_ids=preserve_vvar_ids,
1423 )
1424 # cache the simplifier's RDA analysis
1425 self.reaching_definitions = simp._reaching_definitions
File ./venv/lib/python3.10/site-packages/angr/analyses/analysis.py:264, in AnalysisFactory.__call__(self, *args, **kwargs)
258 show_progressbar = kwargs.pop("show_progressbar", False)
260 w = self.prep(
261 fail_fast=fail_fast, kb=kb, progress_callback=progress_callback, show_progressbar=show_progressbar
262 )
--> 264 r = w(*args, **kwargs)
265 # clean up so that it's always pickleable
266 r._progressbar = None
File ./venv/lib/python3.10/site-packages/angr/analyses/analysis.py:249, in AnalysisFactory.prep.<locals>.wrapper(*args, **kwargs)
246 oself._progress_callback = progress_callback
248 oself._show_progressbar = show_progressbar
--> 249 oself.__init__(*args, **kwargs)
250 return oself
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/ail_simplifier.py:131, in AILSimplifier.__init__(self, func, func_graph, remove_dead_memdefs, stack_arg_offsets, unify_variables, ail_manager, gp, narrow_expressions, only_consts, fold_callexprs_into_conditions, use_callee_saved_regs_at_return, rewrite_ccalls, removed_vvar_ids, arg_vvars, avoid_vvar_ids, secondary_stackvars)
128 self.blocks = {} # Mapping nodes to simplified blocks
130 self.simplified: bool = False
--> 131 self._simplify()
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/ail_simplifier.py:150, in AILSimplifier._simplify(self)
147 self._clear_cache()
149 _l.debug("Folding expressions")
--> 150 folded_exprs = self._fold_exprs()
151 self.simplified |= folded_exprs
152 if folded_exprs:
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/ail_simplifier.py:618, in AILSimplifier._fold_exprs(self)
613 """
614 Fold expressions: Fold assigned expressions that are constant or only used once.
615 """
617 # propagator
--> 618 propagator = self._compute_propagation()
619 replacements = propagator.replacements
621 # take replacements and rebuild the corresponding blocks
File ./venv/lib/python3.10/site-packages/angr/utils/timing.py:72, in timethis.<locals>.timed_func(*args, **kwargs)
70 _on_func_return(func, start)
71 return r
---> 72 return func(*args, **kwargs)
File ./venv/lib/python3.10/site-packages/angr/analyses/decompiler/ail_simplifier.py:227, in AILSimplifier._compute_propagation(self)
225 return self._propagator
226 func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
--> 227 prop = self.project.analyses[SPropagatorAnalysis].prep(fail_fast=self._fail_fast)(
228 subject=self.func,
229 func_graph=self.func_graph,
230 func_args=func_args,
231 # gp=self._gp,
232 only_consts=self._only_consts,
233 )
234 self._propagator = prop
235 self._propagator_dead_vvar_ids = prop.dead_vvar_ids
File ./venv/lib/python3.10/site-packages/angr/analyses/analysis.py:249, in AnalysisFactory.prep.<locals>.wrapper(*args, **kwargs)
246 oself._progress_callback = progress_callback
248 oself._show_progressbar = show_progressbar
--> 249 oself.__init__(*args, **kwargs)
250 return oself
File ./venv/lib/python3.10/site-packages/angr/analyses/s_propagator.py:100, in SPropagatorAnalysis.__init__(self, subject, func_graph, only_consts, stack_pointer_tracker, func_args, func_addr)
97 # output
98 self.model = SPropagatorModel()
--> 100 self._analyze()
File ./venv/lib/python3.10/site-packages/angr/analyses/s_propagator.py:396, in SPropagatorAnalysis._analyze(self)
393 print(tmp_uselocs[block_loc])
395 block = blocks[(block_loc.block_addr, block_loc.block_idx)]
--> 396 tmp_def_stmtidx = tmp_deflocs[block_loc][tmp_atom]
398 stmt = block.statements[tmp_def_stmtidx]
399 if isinstance(stmt, Assignment):
KeyError: <Tmp 2>Findings
I tried to figure out what was going wrong.
The block where it fails is:
## Block 40940f
00 | 0x40940f | LABEL_40940f:
01 | 0x40940f | vvar_2{reg 392} = @32b [((4232201, None), vvar_10{reg 392}), ((4232207, None), vvar_25{reg 392})]
02 | 0x40940f | vvar_3{reg 16} = @32b [((4232201, None), None), ((4232207, None), vvar_13{reg 16})]
03 | 0x40940f | vvar_4{reg 72} = @32b [((4232201, None), None), ((4232207, None), vvar_20{reg 72})]
04 | 0x40940f | vvar_5{reg 76} = @32b [((4232201, None), None), ((4232207, None), vvar_21{reg 76})]
05 | 0x40940f | vvar_6{reg 80} = @32b [((4232201, None), None), ((4232207, None), vvar_22{reg 80})]
06 | 0x40940f | vvar_7{reg 84} = @32b [((4232201, None), None), ((4232207, None), vvar_23{reg 84})]
07 | 0x40940f | vvar_8{reg 12} = @32b [((4232201, None), None), ((4232207, None), vvar_19{reg 12})]
08 | 0x40940f | vvar_11{reg 392} = 0x0<32>
09 | 0x40940f | t25 = vvar_9{reg 20}
10 | 0x40940f | [D] t2 = LDle-Linked(t25)()
11 | 0x40940f | vvar_12{reg 16} = t2
12 | 0x409413 | t26 = vvar_4{reg 72}
13 | 0x409413 | t27 = vvar_5{reg 76}
14 | 0x409413 | t28 = vvar_6{reg 80}
15 | 0x409413 | t29 = vvar_7{reg 84}
16 | 0x409413 | t74 = armg_calculate_flag_v(t26, t27, t28, t29)
17 | 0x409413 | t75 = armg_calculate_flag_c(t26, t27, t28, t29)
18 | 0x409413 | t37 = vvar_0
19 | 0x409413 | t38 = vvar_12{reg 16}
20 | 0x409413 | t36 = (t38 | t37)
21 | 0x409413 | vvar_13{reg 16} = t36
22 | 0x409413 | vvar_14{reg 72} = 0x5<32>
23 | 0x409413 | vvar_15{reg 76} = t36
24 | 0x409413 | vvar_16{reg 80} = t75
25 | 0x409413 | vvar_17{reg 84} = t74
26 | 0x409415 | vvar_18{reg 392} = 0x0<32>
27 | 0x409415 | t53 = vvar_9{reg 20}
28 | 0x409415 | t54 = vvar_13{reg 16}
29 | 0x409415 | [D] t11 = ( STle-Cond(t53) = t54 )()
30 | 0x409415 | t76 = (Not t11)
31 | 0x409415 | t77 = Conv(1->32, t76)
32 | 0x409415 | vvar_19{reg 12} = t77
33 | 0x409419 | t15 = vvar_19{reg 12}
34 | 0x409419 | vvar_20{reg 72} = 0x2<32>
35 | 0x409419 | vvar_21{reg 76} = t15
36 | 0x409419 | vvar_22{reg 80} = 0x0<32>
37 | 0x409419 | vvar_23{reg 84} = 0x0<32>
38 | 0x40941b | vvar_24{reg 392} = 0x0<32>
39 | 0x40941b | vvar_25{reg 392} = 0x0<32>
40 | 0x40941b | t79 = (t15 != 0x0<32>)
41 | 0x40941b | t78 = Conv(1->32, t79)
42 | 0x40941b | t80 = Conv(32->1, t78)
43 | 0x40941b | if (t80) { Goto 0x40940f<32> } else { Goto 0x40941d<32> }As you can see the temp var (t2) is assigned on line 10 as DirtyStatement.
The crash in s_propagator.py (as seen in the trace) occurs because get_vvar_uselocs does include t2, while get_tmp_deflocs does not include t2 because it is a DirtyStatement and not an Assignment.
This is where my understanding stops.
I see two options: either the DirtyStatement should have been removed before entering s_propagator, or get_tmp_deflocs should include t2.
Steps to reproduce the bug
No response
Environment
No response
Additional context
No response
Source: angr/angr