#5474·angr

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

python3
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