angr 9.2.122__py3-none-manylinux2014_x86_64.whl → 9.2.123__py3-none-manylinux2014_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of angr might be problematic. Click here for more details.
- angr/__init__.py +1 -1
- angr/analyses/calling_convention.py +6 -1
- angr/analyses/decompiler/ail_simplifier.py +22 -323
- angr/analyses/decompiler/clinic.py +3 -2
- angr/analyses/decompiler/dephication/rewriting_engine.py +1 -1
- angr/analyses/decompiler/expression_narrower.py +201 -5
- angr/analyses/decompiler/optimization_passes/ite_region_converter.py +11 -7
- angr/analyses/decompiler/optimization_passes/lowered_switch_simplifier.py +10 -1
- angr/analyses/decompiler/peephole_optimizations/const_mull_a_shift.py +73 -42
- angr/analyses/decompiler/region_simplifiers/expr_folding.py +4 -0
- angr/analyses/decompiler/sequence_walker.py +20 -4
- angr/analyses/s_propagator.py +10 -6
- angr/calling_conventions.py +2 -2
- angr/engines/light/engine.py +12 -2
- angr/engines/soot/expressions/instanceOf.py +4 -1
- angr/engines/successors.py +1 -1
- angr/engines/vex/heavy/concretizers.py +47 -47
- angr/engines/vex/heavy/dirty.py +4 -4
- angr/procedures/java_lang/getsimplename.py +4 -1
- angr/procedures/linux_kernel/iovec.py +5 -2
- angr/sim_type.py +3 -1
- {angr-9.2.122.dist-info → angr-9.2.123.dist-info}/METADATA +7 -6
- {angr-9.2.122.dist-info → angr-9.2.123.dist-info}/RECORD +27 -27
- {angr-9.2.122.dist-info → angr-9.2.123.dist-info}/LICENSE +0 -0
- {angr-9.2.122.dist-info → angr-9.2.123.dist-info}/WHEEL +0 -0
- {angr-9.2.122.dist-info → angr-9.2.123.dist-info}/entry_points.txt +0 -0
- {angr-9.2.122.dist-info → angr-9.2.123.dist-info}/top_level.txt +0 -0
|
@@ -42,10 +42,8 @@ class ITERegionConverter(OptimizationPass):
|
|
|
42
42
|
if not ite_assign_regions:
|
|
43
43
|
break
|
|
44
44
|
|
|
45
|
-
for region_head, region_tail,
|
|
46
|
-
round_update |= self._convert_region_to_ternary_expr(
|
|
47
|
-
region_head, region_tail, true_block, true_stmt, false_block, false_stmt
|
|
48
|
-
)
|
|
45
|
+
for region_head, region_tail, _, true_stmt, _, false_stmt in ite_assign_regions:
|
|
46
|
+
round_update |= self._convert_region_to_ternary_expr(region_head, region_tail, true_stmt, false_stmt)
|
|
49
47
|
|
|
50
48
|
if not round_update:
|
|
51
49
|
break
|
|
@@ -188,9 +186,7 @@ class ITERegionConverter(OptimizationPass):
|
|
|
188
186
|
self,
|
|
189
187
|
region_head,
|
|
190
188
|
region_tail,
|
|
191
|
-
true_block,
|
|
192
189
|
true_stmt: Assignment | Call,
|
|
193
|
-
false_block,
|
|
194
190
|
false_stmt: Assignment | Call,
|
|
195
191
|
):
|
|
196
192
|
if region_head not in self._graph or region_tail not in self._graph:
|
|
@@ -242,6 +238,14 @@ class ITERegionConverter(OptimizationPass):
|
|
|
242
238
|
#
|
|
243
239
|
|
|
244
240
|
region_nodes = subgraph_between_nodes(self._graph, region_head, [region_tail])
|
|
241
|
+
|
|
242
|
+
# we must obtain the predecessors of the region tail instead of using true_block and false_block because
|
|
243
|
+
# true_block and false_block may have other successors before reaching the region tail!
|
|
244
|
+
region_tail_preds = [pred for pred in self._graph.predecessors(region_tail) if pred in region_nodes]
|
|
245
|
+
if len(region_tail_preds) != 2:
|
|
246
|
+
return False
|
|
247
|
+
region_tail_pred_srcs = {(pred.addr, pred.idx) for pred in region_tail_preds}
|
|
248
|
+
|
|
245
249
|
for node in region_nodes:
|
|
246
250
|
if node is region_head or node is region_tail:
|
|
247
251
|
continue
|
|
@@ -259,7 +263,7 @@ class ITERegionConverter(OptimizationPass):
|
|
|
259
263
|
continue
|
|
260
264
|
new_src_and_vvars = []
|
|
261
265
|
for src, vvar in stmt.src.src_and_vvars:
|
|
262
|
-
if src not in
|
|
266
|
+
if src not in region_tail_pred_srcs:
|
|
263
267
|
new_src_and_vvars.append((src, vvar))
|
|
264
268
|
new_vvar = new_assignment.dst.copy()
|
|
265
269
|
new_src_and_vvars.append(((region_head.addr, region_head.idx), new_vvar))
|
|
@@ -396,7 +396,16 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
|
|
|
396
396
|
default_case_candidates = {}
|
|
397
397
|
last_comp = None
|
|
398
398
|
stack = [(head, 0, 0xFFFF_FFFF_FFFF_FFFF)]
|
|
399
|
-
|
|
399
|
+
|
|
400
|
+
# cursed: there is an infinite loop in the following loop that
|
|
401
|
+
# occurs rarely. we need to keep track of the nodes we've seen
|
|
402
|
+
# to break out of the loop.
|
|
403
|
+
# See https://github.com/angr/angr/pull/4953
|
|
404
|
+
#
|
|
405
|
+
# FIXME: the root cause should be fixed and this workaround removed
|
|
406
|
+
seen = set()
|
|
407
|
+
while stack and tuple(stack) not in seen:
|
|
408
|
+
seen.add(tuple(stack))
|
|
400
409
|
comp, min_, max_ = stack.pop(0)
|
|
401
410
|
(
|
|
402
411
|
comp_type,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylint:disable=too-many-boolean-expressions
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from ailment.expression import Convert, BinaryOp, Const
|
|
4
|
+
from ailment.expression import Convert, BinaryOp, Const, Expression
|
|
5
5
|
|
|
6
6
|
from .base import PeepholeOptimizationExprBase
|
|
7
7
|
|
|
@@ -56,47 +56,10 @@ class ConstMullAShift(PeepholeOptimizationExprBase):
|
|
|
56
56
|
|
|
57
57
|
elif isinstance(expr, BinaryOp) and expr.op in {"Add", "Sub"}:
|
|
58
58
|
expr0, expr1 = expr.operands
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
and isinstance(expr1, BinaryOp)
|
|
64
|
-
and expr1.op in {"Shr", "Sar"}
|
|
65
|
-
and isinstance(expr1.operands[1], Const)
|
|
66
|
-
):
|
|
67
|
-
if (
|
|
68
|
-
isinstance(expr0.operands[0], BinaryOp)
|
|
69
|
-
and expr0.operands[0].op in {"Mull", "Mul"}
|
|
70
|
-
and isinstance(expr0.operands[0].operands[1], Const)
|
|
71
|
-
):
|
|
72
|
-
a0 = expr0.operands[0].operands[0]
|
|
73
|
-
a1 = expr1.operands[0]
|
|
74
|
-
elif (
|
|
75
|
-
isinstance(expr1.operands[0], BinaryOp)
|
|
76
|
-
and expr1.operands[0].op in {"Mull", "Mul"}
|
|
77
|
-
and isinstance(expr1.operands[0].operands[1], Const)
|
|
78
|
-
):
|
|
79
|
-
a1 = expr0.operands[0].operands[0]
|
|
80
|
-
a0 = expr1.operands[0]
|
|
81
|
-
else:
|
|
82
|
-
a0, a1 = None, None
|
|
83
|
-
if a0 is not None and a1 is not None and a0.likes(a1):
|
|
84
|
-
# (a * x >> M1) +/- (a >> M2) ==> a / N
|
|
85
|
-
C = expr0.operands[0].operands[1].value
|
|
86
|
-
X = a0
|
|
87
|
-
V = expr0.operands[1].value
|
|
88
|
-
ndigits = 5 if V == 32 else 6
|
|
89
|
-
divisor = self._check_divisor(pow(2, V), C, ndigits)
|
|
90
|
-
if divisor is not None:
|
|
91
|
-
new_const = Const(None, None, divisor, X.bits)
|
|
92
|
-
# we cannot drop the convert in this case
|
|
93
|
-
return BinaryOp(
|
|
94
|
-
expr0.operands[0].idx,
|
|
95
|
-
"Div",
|
|
96
|
-
[X, new_const],
|
|
97
|
-
expr0.operands[0].signed,
|
|
98
|
-
**expr0.operands[0].tags,
|
|
99
|
-
)
|
|
59
|
+
if isinstance(expr1, Convert) and expr1.from_bits == 32 and expr1.to_bits == 64:
|
|
60
|
+
r = self._match_case_a(expr0, expr1)
|
|
61
|
+
if r is not None:
|
|
62
|
+
return r
|
|
100
63
|
|
|
101
64
|
# with Convert in consideration
|
|
102
65
|
if (
|
|
@@ -149,6 +112,74 @@ class ConstMullAShift(PeepholeOptimizationExprBase):
|
|
|
149
112
|
|
|
150
113
|
return None
|
|
151
114
|
|
|
115
|
+
def _match_case_a(self, expr0: Expression, expr1: Convert) -> BinaryOp | None:
|
|
116
|
+
# (
|
|
117
|
+
# (((Conv(32->64, vvar_44{reg 32}) * 0x4325c53f<64>) >>a 0x24<8>) & 0xffffffff<64>) -
|
|
118
|
+
# Conv(32->s64, (vvar_44{reg 32} >>a 0x1f<8>))
|
|
119
|
+
# )
|
|
120
|
+
|
|
121
|
+
expr1 = expr1.operand
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
isinstance(expr0, BinaryOp)
|
|
125
|
+
and expr0.op == "And"
|
|
126
|
+
and isinstance(expr0.operands[1], Const)
|
|
127
|
+
and expr0.operands[1].value == 0xFFFFFFFF
|
|
128
|
+
):
|
|
129
|
+
expr0 = expr0.operands[0]
|
|
130
|
+
else:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
isinstance(expr0, BinaryOp)
|
|
135
|
+
and expr0.op in {"Shr", "Sar"}
|
|
136
|
+
and isinstance(expr0.operands[1], Const)
|
|
137
|
+
and isinstance(expr1, BinaryOp)
|
|
138
|
+
and expr1.op in {"Shr", "Sar"}
|
|
139
|
+
and isinstance(expr1.operands[1], Const)
|
|
140
|
+
):
|
|
141
|
+
if (
|
|
142
|
+
isinstance(expr0.operands[0], BinaryOp)
|
|
143
|
+
and expr0.operands[0].op in {"Mull", "Mul"}
|
|
144
|
+
and isinstance(expr0.operands[0].operands[1], Const)
|
|
145
|
+
):
|
|
146
|
+
a0 = expr0.operands[0].operands[0]
|
|
147
|
+
a1 = expr1.operands[0]
|
|
148
|
+
elif (
|
|
149
|
+
isinstance(expr1.operands[0], BinaryOp)
|
|
150
|
+
and expr1.operands[0].op in {"Mull", "Mul"}
|
|
151
|
+
and isinstance(expr1.operands[0].operands[1], Const)
|
|
152
|
+
):
|
|
153
|
+
a1 = expr0.operands[0].operands[0]
|
|
154
|
+
a0 = expr1.operands[0]
|
|
155
|
+
else:
|
|
156
|
+
a0, a1 = None, None
|
|
157
|
+
|
|
158
|
+
# a0: Conv(32->64, vvar_44{reg 32})
|
|
159
|
+
# a1: vvar_44{reg 32}
|
|
160
|
+
if isinstance(a0, Convert) and a0.from_bits == a1.bits:
|
|
161
|
+
a0 = a0.operand
|
|
162
|
+
|
|
163
|
+
if a0 is not None and a1 is not None and a0.likes(a1):
|
|
164
|
+
# (a * x >> M1) +/- (a >> M2) ==> a / N
|
|
165
|
+
C = expr0.operands[0].operands[1].value
|
|
166
|
+
X = a0
|
|
167
|
+
V = expr0.operands[1].value
|
|
168
|
+
ndigits = 5 if V == 32 else 6
|
|
169
|
+
divisor = self._check_divisor(pow(2, V), C, ndigits)
|
|
170
|
+
if divisor is not None:
|
|
171
|
+
new_const = Const(None, None, divisor, X.bits)
|
|
172
|
+
# we cannot drop the convert in this case
|
|
173
|
+
return BinaryOp(
|
|
174
|
+
expr0.operands[0].idx,
|
|
175
|
+
"Div",
|
|
176
|
+
[X, new_const],
|
|
177
|
+
expr0.operands[0].signed,
|
|
178
|
+
**expr0.operands[0].tags,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
152
183
|
@staticmethod
|
|
153
184
|
def _check_divisor(a, b, ndigits=6):
|
|
154
185
|
divisor_1 = 1 + (a // b)
|
|
@@ -395,6 +395,10 @@ class ExpressionReplacer(AILBlockWalker):
|
|
|
395
395
|
|
|
396
396
|
def _handle_Assignment(self, stmt_idx: int, stmt: Assignment, block: Block | None):
|
|
397
397
|
# override the base handler and make sure we do not replace .dst with a Call expression or an ITE expression
|
|
398
|
+
|
|
399
|
+
if is_phi_assignment(stmt):
|
|
400
|
+
return None
|
|
401
|
+
|
|
398
402
|
changed = False
|
|
399
403
|
|
|
400
404
|
dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
|
|
@@ -216,11 +216,27 @@ class SequenceWalker:
|
|
|
216
216
|
)
|
|
217
217
|
|
|
218
218
|
def _handle_CascadingCondition(self, node: CascadingConditionNode, **kwargs):
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
cond_nodes_changed = False
|
|
220
|
+
new_condition_and_nodes = []
|
|
221
|
+
for index, (cond, child_node) in enumerate(node.condition_and_nodes):
|
|
222
|
+
new_child = self._handle(child_node, parent=node, index=index)
|
|
223
|
+
if new_child is not None:
|
|
224
|
+
cond_nodes_changed = True
|
|
225
|
+
new_condition_and_nodes.append((cond, new_child))
|
|
226
|
+
else:
|
|
227
|
+
new_condition_and_nodes.append((cond, child_node))
|
|
228
|
+
|
|
229
|
+
new_else = None
|
|
221
230
|
if node.else_node is not None:
|
|
222
|
-
self._handle(node.else_node, parent=node, index=-1)
|
|
223
|
-
|
|
231
|
+
new_else = self._handle(node.else_node, parent=node, index=-1)
|
|
232
|
+
|
|
233
|
+
if cond_nodes_changed or new_else is not None:
|
|
234
|
+
return CascadingConditionNode(
|
|
235
|
+
node.addr,
|
|
236
|
+
new_condition_and_nodes if cond_nodes_changed else node.condition_and_nodes,
|
|
237
|
+
else_node=new_else if new_else is not None else node.else_node,
|
|
238
|
+
)
|
|
239
|
+
return None
|
|
224
240
|
|
|
225
241
|
def _handle_ConditionalBreak(self, node: ConditionalBreakNode, **kwargs): # pylint:disable=no-self-use
|
|
226
242
|
return None
|
angr/analyses/s_propagator.py
CHANGED
|
@@ -5,7 +5,7 @@ from collections import defaultdict
|
|
|
5
5
|
|
|
6
6
|
from ailment.block import Block
|
|
7
7
|
from ailment.expression import Const, VirtualVariable, VirtualVariableCategory, StackBaseOffset
|
|
8
|
-
from ailment.statement import Assignment, Store, Return
|
|
8
|
+
from ailment.statement import Assignment, Store, Return, Jump
|
|
9
9
|
|
|
10
10
|
from angr.knowledge_plugins.functions import Function
|
|
11
11
|
from angr.code_location import CodeLocation
|
|
@@ -98,11 +98,15 @@ class SPropagatorAnalysis(Analysis):
|
|
|
98
98
|
# find all vvar uses
|
|
99
99
|
vvar_uselocs = get_vvar_uselocs(blocks.values())
|
|
100
100
|
|
|
101
|
-
# find all ret sites
|
|
101
|
+
# find all ret sites and indirect jump sites
|
|
102
102
|
retsites: set[tuple[int, int | None, int]] = set()
|
|
103
|
+
jumpsites: set[tuple[int, int | None, int]] = set()
|
|
103
104
|
for bb in blocks.values():
|
|
104
|
-
if bb.statements
|
|
105
|
-
|
|
105
|
+
if bb.statements:
|
|
106
|
+
if isinstance(bb.statements[-1], Return):
|
|
107
|
+
retsites.add((bb.addr, bb.idx, len(bb.statements) - 1))
|
|
108
|
+
elif isinstance(bb.statements[-1], Jump):
|
|
109
|
+
jumpsites.add((bb.addr, bb.idx, len(bb.statements) - 1))
|
|
106
110
|
|
|
107
111
|
replacements = defaultdict(dict)
|
|
108
112
|
|
|
@@ -169,13 +173,13 @@ class SPropagatorAnalysis(Analysis):
|
|
|
169
173
|
{
|
|
170
174
|
loc
|
|
171
175
|
for _, loc in vvar_uselocs[vvar.varid]
|
|
172
|
-
if (loc.block_addr, loc.block_idx, loc.stmt_idx) not in retsites
|
|
176
|
+
if (loc.block_addr, loc.block_idx, loc.stmt_idx) not in (retsites | jumpsites)
|
|
173
177
|
}
|
|
174
178
|
)
|
|
175
179
|
== 1
|
|
176
180
|
):
|
|
177
181
|
if is_const_and_vvar_assignment(stmt):
|
|
178
|
-
# this vvar is used once if we exclude its uses at ret sites. we can propagate it
|
|
182
|
+
# this vvar is used once if we exclude its uses at ret sites or jump sites. we can propagate it
|
|
179
183
|
for vvar_used, vvar_useloc in vvar_uselocs[vvar.varid]:
|
|
180
184
|
replacements[vvar_useloc][vvar_used] = stmt.src
|
|
181
185
|
|
angr/calling_conventions.py
CHANGED
|
@@ -1212,7 +1212,7 @@ class SimCCCdecl(SimCC):
|
|
|
1212
1212
|
if isinstance(arg_type, (SimTypeArray, SimTypeFixedSizeArray)): # hack
|
|
1213
1213
|
arg_type = SimTypePointer(arg_type.elem_type).with_arch(self.arch)
|
|
1214
1214
|
locs_size = 0
|
|
1215
|
-
byte_size = arg_type.size // self.arch.byte_width
|
|
1215
|
+
byte_size = arg_type.size // self.arch.byte_width if arg_type.size is not None else self.arch.bytes
|
|
1216
1216
|
locs = []
|
|
1217
1217
|
while locs_size < byte_size:
|
|
1218
1218
|
locs.append(next(session.both_iter))
|
|
@@ -1293,7 +1293,7 @@ class SimCCMicrosoftAMD64(SimCC):
|
|
|
1293
1293
|
except StopIteration:
|
|
1294
1294
|
int_loc = fp_loc = next(session.both_iter)
|
|
1295
1295
|
|
|
1296
|
-
byte_size = arg_type.size // self.arch.byte_width
|
|
1296
|
+
byte_size = arg_type.size // self.arch.byte_width if arg_type.size is not None else self.arch.bytes
|
|
1297
1297
|
|
|
1298
1298
|
if isinstance(arg_type, SimTypeFloat):
|
|
1299
1299
|
return fp_loc.refine(size=byte_size, is_fp=True, arch=self.arch)
|
angr/engines/light/engine.py
CHANGED
|
@@ -604,6 +604,9 @@ class SimEngineLightVEXMixin(SimEngineLightMixin):
|
|
|
604
604
|
if self._is_top(expr_0) or self._is_top(expr_1):
|
|
605
605
|
return self._top(expr.result_size(self.tyenv))
|
|
606
606
|
|
|
607
|
+
if expr_1.concrete and expr_1.concrete_value == 0:
|
|
608
|
+
return self._top(expr.result_size(self.tyenv))
|
|
609
|
+
|
|
607
610
|
signed = "U" in expr.op # Iop_DivModU64to32 vs Iop_DivMod
|
|
608
611
|
from_size = expr_0.size()
|
|
609
612
|
to_size = expr_1.size()
|
|
@@ -632,10 +635,13 @@ class SimEngineLightVEXMixin(SimEngineLightMixin):
|
|
|
632
635
|
if self._is_top(expr_0) or self._is_top(expr_1):
|
|
633
636
|
return self._top(expr_0.size())
|
|
634
637
|
|
|
638
|
+
if expr_1.concrete and expr_1.concrete_value == 0:
|
|
639
|
+
return self._top(expr.result_size(self.tyenv))
|
|
640
|
+
|
|
635
641
|
try:
|
|
636
642
|
return expr_0 / expr_1
|
|
637
643
|
except ZeroDivisionError:
|
|
638
|
-
return self._top(
|
|
644
|
+
return self._top(expr.result_size(self.tyenv))
|
|
639
645
|
|
|
640
646
|
def _handle_Mod(self, expr):
|
|
641
647
|
args, r = self._binop_get_args(expr)
|
|
@@ -646,6 +652,9 @@ class SimEngineLightVEXMixin(SimEngineLightMixin):
|
|
|
646
652
|
if self._is_top(expr_0) or self._is_top(expr_1):
|
|
647
653
|
return self._top(expr_0.size())
|
|
648
654
|
|
|
655
|
+
if expr_1.concrete and expr_1.concrete_value == 0:
|
|
656
|
+
return self._top(expr.result_size(self.tyenv))
|
|
657
|
+
|
|
649
658
|
try:
|
|
650
659
|
return expr_0 - (expr_1 // expr_1) * expr_1
|
|
651
660
|
except ZeroDivisionError:
|
|
@@ -974,7 +983,8 @@ class SimEngineLightAILMixin(SimEngineLightMixin):
|
|
|
974
983
|
return expr
|
|
975
984
|
|
|
976
985
|
def _ail_handle_CallExpr(self, expr: ailment.Stmt.Call):
|
|
977
|
-
|
|
986
|
+
if not isinstance(expr.target, str):
|
|
987
|
+
self._expr(expr.target)
|
|
978
988
|
return expr
|
|
979
989
|
|
|
980
990
|
def _ail_handle_Reinterpret(self, expr: ailment.Expr.Reinterpret):
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
|
|
5
|
+
import claripy
|
|
6
|
+
|
|
4
7
|
from .base import SimSootExpr
|
|
5
8
|
|
|
6
9
|
l = logging.getLogger(name=__name__)
|
|
@@ -9,4 +12,4 @@ l = logging.getLogger(name=__name__)
|
|
|
9
12
|
class SimSootExpr_InstanceOf(SimSootExpr):
|
|
10
13
|
def _execute(self):
|
|
11
14
|
obj = self._translate_value(self.expr.value)
|
|
12
|
-
self.expr =
|
|
15
|
+
self.expr = claripy.StringV(obj.type) == claripy.StringV(self.expr.check_type)
|
angr/engines/successors.py
CHANGED
|
@@ -504,7 +504,7 @@ class SimSuccessors:
|
|
|
504
504
|
fallback = True
|
|
505
505
|
break
|
|
506
506
|
|
|
507
|
-
cond_and_targets.append((cond, target if not outer_reverse else
|
|
507
|
+
cond_and_targets.append((cond, target if not outer_reverse else claripy.Reverse(target)))
|
|
508
508
|
|
|
509
509
|
if reached_sentinel is False:
|
|
510
510
|
# huh?
|