from __future__ import annotations
from typing import Dict, Generator, List, Tuple, Set
# both above imports only for type hints

from copy import deepcopy
from rand import random
from sys import stdout
from time import time

import scipy as sp

from parsebin import *
from utils import to_bytes
from logger import Logger

from rop_platform import Platform
from stackview import Stack_view
from structured_element import Structured_element_ARM64
from effect import Effect_ARM64

from capstone import *
import z3

# NOTE: everything here currently is WIP
#       for a stable implementation (x86 64) use roputils.py from main branch

# NOTE: any "address"-related keyword used does NOT take into accound ASLR / PIE

# gadget class that has associated a stack view and its effects
# a gadget object can store two identical gadgets, but at different addresses
class ROP_gadget_ARM64:

    # maximum gadget byte length to be searched for
    MAX_GADGET_BYTE_LEN = 60

    def __init__(self):

        self.stack = Stack_view()
        self.effects: List[Effect_ARM64] = []

        # self.b - bytes of the gadget
        # useful for identification (hashing) of a gadget / chain
        self.b: bytes = None

        # self.addrs - addresses of identical gadgets
        # used only at payload generation
        self.addrs: List[int] = []

        # self.eq_g - gadgets that differ by stack padding or nop instructions
        # used only at payload generation
        self.eq_g: List[ROP_chain_ARM64] = []

        # self.end_sp_pos - location of stack pointer
        # after exiting from the gadget
        # (always: start sp pos == 0)
        self.end_sp_pos: int = 0

        # redundant, to be used for fast checks
        # whether all stack references are fully defined
        # and whether this gadget / chain can jump
        self.valid_stack_access: bool = False
        self.valid_jump: bool = False

        # store every unresolved deref,
        # even when it is not found (anymore) in the self.effects list
        # (the access still takes place, even if the extracted value
        #   is no longer relevant when exiting the gadget / chain)
        # when calculating valid stack access, all items inside
        # self.uinresolved_derefs should be taken into account
        self.unresolved_derefs: List[Effect_ARM64] = []

    def get_bytes(self):
        return self.b

    def get_stack_size(self):

        # NOTE anything not in self.stack.elements is just padding
        #       which does not need to be included in the final payload
        # return max(len(self.stack.elements), self.end_sp_pos + 1)

        return len(self.stack.elements)

    # by default, it contains the addresses without ASLR/PIE offsets
    def get_current_addrs(self):
        return [Stack_view.stack_values[addr] for addr in self.addrs]

    def show(self, capstone_handle: Cs = None, show_addr = True, show_stack = True, output_handle = stdout):

        if len(self.addrs) == 0:
            print("(empty gadget)", file = output_handle)
            return

        print(f"valid_stack_access = {self.valid_stack_access}", file = output_handle)
        print(f"valid_jump = {self.valid_jump}\n", file = output_handle)

        if capstone_handle is None:
            capstone_handle = Cs(CS_ARCH_ARM64, CS_MODE_ARM)

        disas_instr_generator = capstone_handle.disasm(self.b, self.get_current_addrs()[0])
        for ins in disas_instr_generator:

            if show_addr is True:
                print(f"{hex(ins.address)}: {ins.mnemonic} {ins.op_str}", file = output_handle)
            else:
                print(f"{ins.mnemonic} {ins.op_str}", file = output_handle)

        if show_stack is True:

            _s = f"----- STACK ({self.get_stack_size()} ELEMENTS) -----"
            print(_s, file = output_handle)

            self._show_stack_values(output_handle)

            print("-" * len(_s), file = output_handle)

    def _show_stack_values(self, output_handle):

        for idx, el in enumerate(self.stack.elements):

            if idx == self.end_sp_pos:
                suffix = "<---- end SP"
            else:
                suffix = ""

            if el.type == "64b_stack_val":

                val = Stack_view.stack_values[el.info['id']]
                jmp = Stack_view.related_jump[el.info['id']]

                if jmp is not None:
                    suffix = f"(jump {jmp}) {suffix}"

                if val is not None:
                    print(f"(+{hex(idx * 8)}) id {el.info['id']}: {hex(val)} {suffix}", file = output_handle)
                else:
                    print(f"(+{hex(idx * 8)}) id {el.info['id']}: EMPTY {suffix}", file = output_handle)
            else:
                print(f"(+{hex(idx * 8)}) ====PAD==== {suffix}", file = output_handle)

        print(f"\nend SP offset: +{hex(8 * self.end_sp_pos)}")

    def add_current_addr(self, addr: int):

        new_addr_id = Stack_view.get_elem_id()
        self.addrs.append(new_addr_id)
        Stack_view.stack_values[new_addr_id] = addr

    # auxiliary internal method for duplication
    def _duplicate_stack(self, cpy: ROP_chain_ARM64 | ROP_chain_ARM64, copy_stack_associated_values):

        old_new_id: Dict[int, int] = {}
        def _get_new_id(old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        # recursive search for stack elements that need to be replaced
        def _recursive_replace(op_element: Structured_element_ARM64):

            if op_element.type == "64b_stack_val":
                op_element.info["id"] = _get_new_id(op_element.info["id"])

            elif op_element.is_op():

                if op_element.info["term_1"] is not None:
                    _recursive_replace(op_element.info["term_1"])

                if op_element.info["term_2"] is not None:
                    _recursive_replace(op_element.info["term_2"])

        for stack_elem in self.stack.elements:

            if stack_elem.type == "64b_stack_pad":
                cpy.stack.push(Structured_element_ARM64.instantiate_structured_element("64b_stack_pad"))

            elif stack_elem.type == "64b_stack_val":

                cpy_stack_elem = Structured_element_ARM64.instantiate_structured_element("64b_stack_val")

                cpy_id = _get_new_id(stack_elem.info["id"])
                if cpy_id is not None:
                    raise Exception(f"double stack id found when duplicating stack {cpy_id}")

                cpy_stack_elem.info["id"] = Stack_view.get_elem_id()
                old_new_id.update({stack_elem.info["id"]: cpy_stack_elem.info["id"]})

                if copy_stack_associated_values is True:

                    Stack_view.stack_values[cpy_stack_elem.info["id"]] = Stack_view.stack_values[stack_elem.info["id"]]
                    Stack_view.related_jump[cpy_stack_elem.info["id"]] = Stack_view.related_jump[stack_elem.info["id"]]

                cpy.stack.push(cpy_stack_elem)

        cpy.effects = deepcopy(self.effects)
        for ef in cpy.effects:

            if ef.type in ["ARITH", "LOAD_S", "JUMP"]:
                _recursive_replace(ef.params[0])

        # unresolved derefs might directly access stack, too
        cpy.unresolved_derefs = deepcopy(self.unresolved_derefs)
        for el in cpy.unresolved_derefs:
            _recursive_replace(el)

        return cpy, old_new_id

    # a gadget has fixed stack element ids that are kept globally
    # so to use multiple times the same gadget,
    # a duplicate method is needed, that automatically
    # makes a deep copy of the stack elements and ids, and also the effects
    # it returns the new copy and the old_new_id list
    # NOTE: if the old id had an associated value, it also copies it, if chosen so
    # NOTE: does NOT duplicate the elements from eq_g
    def duplicate(self, copy_stack_associated_values = True):

        cpy = ROP_gadget_ARM64()

        cpy.b = self.b

        cpy.eq_g = self.eq_g.copy()

        cpy.valid_stack_access = self.valid_stack_access
        cpy.valid_jump = self.valid_jump

        cpy.end_sp_pos = self.end_sp_pos

        cpy.addrs = self.addrs.copy()
        for i in range(len(cpy.addrs)):

            # no related jump for self.addrs IDs

            addr_val = Stack_view.stack_values[cpy.addrs[i]]
            addr_id_cpy = Stack_view.get_elem_id()
            Stack_view.stack_values[addr_id_cpy] = addr_val
            cpy.addrs[i] = addr_id_cpy

        return self._duplicate_stack(cpy, copy_stack_associated_values)

    # this method should ONLY be called when you DO NOT NEED THE GADGET ANYMORE
    # it clears the stack and removes the id s that are also present in the corresponding dictionary
    # so that no memory is leaked
    # NOTE: does not remove anything from eq_g
    def remove_stack_ids(self):

        self.b = None
        self.effects = None

        for addr_id in self.addrs:
            Stack_view.del_id(addr_id)

        for stack_elem in self.stack.elements:
            if stack_elem.type == "64b_stack_val":
                Stack_view.del_id(stack_elem.info["id"])

        self.stack = None
        self.end_sp_pos = None

        self.valid_jump = None
        self.valid_stack_access = None

    # function to check whether the given registers remain unchanged or not
    def check_fixed_regs(self, fixed_reg_list: List[str]):

        for fixed_r in fixed_reg_list:
            for ef in self.effects:

                if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == fixed_r:

                    if ef.type in ["LOAD_CT", "MOV_RR", "LOAD_S"]:
                        return False

                    elif ef.type == "ARITH":

                        nop_mov = Effect_ARM64.make_mov_rr_effect(fixed_r, fixed_r)
                        if Effect_ARM64._match_arith(nop_mov, ef) is False:
                            return False

        return True

    # auxiliary internal method for joining gadgets / chains
    # also handles validation checks / updates
    @staticmethod
    def _join_ef_stk(fst: ROP_chain_ARM64 | ROP_chain_ARM64, snd: ROP_chain_ARM64 | ROP_chain_ARM64):

        # NOTE: returns old_id if not replaced
        def _get_new_id(old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]
            else:
                return old_id

        # recursive search for stack elements that need to be replaced
        def _recursive_replace(op_element: Structured_element_ARM64):

            if op_element.type == "64b_stack_val":
                op_element.info["id"] = _get_new_id(op_element.info["id"])

            elif op_element.is_op():

                if op_element.info["term_1"] is not None:
                    _recursive_replace(op_element.info["term_1"])

                if op_element.info["term_2"] is not None:
                    _recursive_replace(op_element.info["term_2"])

            elif op_element.type == "deref":

                if op_element.info["expr"] is None:
                    raise Exception("dereferencing None found")

                _recursive_replace(op_element.info["expr"])

        fst_cpy, _ = fst.duplicate(copy_stack_associated_values=True)
        snd_cpy, _ = snd.duplicate(copy_stack_associated_values=True)

        joined_effects = Effect_ARM64.join_effects(fst_cpy.effects, snd_cpy.effects)
        joined_unres_derefs = Effect_ARM64.join_unresolved_derefs(fst_cpy.effects, fst_cpy.unresolved_derefs,
                                                                    snd_cpy.unresolved_derefs)
        joined_stack, old_new_id, new_end_sp_pos = Stack_view.join_stacks_overlap(fst_cpy.stack, snd_cpy.stack,
                                                                                fst.end_sp_pos, snd.end_sp_pos)
        if joined_stack is None:

            fst_cpy.remove_stack_ids()
            snd_cpy.remove_stack_ids()

            return None, None, None

        # invalidate them to be sure
        # they are not used anymore, after stack joining
        fst_cpy.stack = None
        snd_cpy.stack = None

        for ef in joined_effects:
            _recursive_replace(ef.params[0])

        for el in joined_unres_derefs:
            _recursive_replace(el)

        valid_stack_access = True
        if fst.valid_stack_access is False or snd.valid_stack_access is False:

            for ef in joined_effects:

                resolved = Effect_ARM64.resolve_stack_access(ef.params[0], joined_stack)
                if resolved is None:

                    for el in joined_stack.elements:
                        if el.type == "64b_stack_val":
                            Stack_view.del_id(el.info["id"])

                    return None, None, None

                valid_stack_access = valid_stack_access and resolved

            old_joined_derefs = joined_unres_derefs
            joined_unres_derefs = []

            for el in old_joined_derefs:

                resolved = Effect_ARM64.resolve_stack_access(el, joined_stack)
                if resolved is None:

                    for el_ in joined_stack.elements:
                        if el_.type == "64b_stack_val":
                            Stack_view.del_id(el_.info["id"])

                    return None, None, None

                if resolved is False:
                    joined_unres_derefs.append(el)

                valid_stack_access = valid_stack_access and resolved

        valid_jump = True
        if fst.valid_jump is False or snd.valid_jump is False:

            for ef in joined_effects:

                if ef.type == "JUMP":

                    valid = Effect_ARM64.resolve_jump(ef.params[0], ef.destination_element.info["value"])
                    if valid is None:

                        for el in joined_stack.elements:
                            if el.type == "64b_stack_val":
                                Stack_view.del_id(el.info["id"])

                        return None, None, None

                    valid_jump = valid_jump and valid

        res_chain = ROP_chain_ARM64()

        res_chain.stack = joined_stack
        res_chain.effects = joined_effects

        res_chain.valid_stack_access = valid_stack_access
        res_chain.valid_jump = valid_jump

        res_chain.end_sp_pos = new_end_sp_pos

        res_chain.unresolved_derefs = joined_unres_derefs

        return res_chain, fst_cpy, snd_cpy

    def join(self, snd: ROP_chain_ARM64 | ROP_chain_ARM64) -> ROP_chain_ARM64:

        fst_cpy: ROP_chain_ARM64
        res_chain, fst_cpy, snd_cpy = ROP_chain_ARM64._join_ef_stk(self, snd)

        if res_chain is None:
            return None

        if type(snd) == ROP_chain_ARM64:

            res_chain.b = [fst_cpy.b] + snd_cpy.b
            res_chain.addrs = [fst_cpy.addrs] + snd_cpy.addrs
            res_chain.eq_g = [fst_cpy.eq_g] + snd_cpy.eq_g

        else:

            res_chain.b = [fst_cpy.b, snd_cpy.b]
            res_chain.addrs = [fst_cpy.addrs, snd_cpy.addrs]
            res_chain.eq_g = [fst_cpy.eq_g, snd_cpy.eq_g]

        return res_chain

    # mostly for debugging purposes
    def __str__(self):
        return f"ROP gadget with stack {self.stack}, addresses are {self.get_current_addrs()}, effects {[str(ef) for ef in self.effects]}"

# class to store rop chains,
# in almost the same way as rop gadgets
class ROP_chain_ARM64(ROP_gadget_ARM64):

    def __init__(self):

        # NOTE: gadgets_stackview_offset absent from ARM64

        self.stack: Stack_view = Stack_view()
        self.effects: List[Effect_ARM64] = []

        self.b: List[bytes] = []

        self.addrs: List[List[int]] = []
        self.eq_g: List[List[ROP_chain_ARM64]] = []

        self.end_sp_pos: int = 0

        self.valid_stack_access: bool = False
        self.valid_jump: bool = False

        self.unresolved_derefs: List[Effect_ARM64] = []

    # converts a gadget to a chain with only one gadget
    # does NOT copy
    @staticmethod
    def convert(gadget: ROP_gadget_ARM64) -> ROP_chain_ARM64:

        # no conversion needed
        if type(gadget) == ROP_chain_ARM64:
            return gadget

        chain = ROP_chain_ARM64()

        chain.effects = gadget.effects
        chain.stack = gadget.stack

        chain.b = [gadget.b]
        chain.addrs = [gadget.addrs]
        chain.eq_g = [gadget.eq_g]

        chain.end_sp_pos = gadget.end_sp_pos

        chain.valid_jump = gadget.valid_jump
        chain.valid_stack_access = gadget.valid_stack_access

        return chain

    def get_bytes(self):

        acc_b = b''
        for b_ in self.b:
            acc_b += b_

        return acc_b

    def get_gadget_cnt(self):
        return len(self.b)

    # generator instead of function as in ROP_gadget_ARM64 class
    def get_current_addrs(self):
        for i in range(self.get_gadget_cnt()):
            yield [Stack_view.stack_values[addr_id] for addr_id in self.addrs[i]]

    def show(self, capstone_handle: Cs = None, show_addr = True, show_stack = True, output_handle = stdout):

        if len(self.addrs) == 0:
            print("(empty chain)", file = output_handle)
            return

        if capstone_handle is None:
            capstone_handle = Cs(CS_ARCH_ARM64, CS_MODE_ARM)

        print(f"valid_stack_access = {self.valid_stack_access}", file = output_handle)
        print(f"valid_jump = {self.valid_jump}\n", file = output_handle)

        _i = 0
        for addrs in self.get_current_addrs():

            disas_instr_generator = capstone_handle.disasm(self.b[_i], addrs[0])
            for ins in disas_instr_generator:

                if show_addr is True:
                    print(f"{hex(ins.address)}: {ins.mnemonic} {ins.op_str}")
                else:
                    print(f"{ins.mnemonic} {ins.op_str}")

            _i += 1

        if show_stack is True:

            _s = f"----- STACK ({self.get_stack_size()} ELEMENTS) -----"
            print(_s, file = output_handle)

            self._show_stack_values(output_handle)

            print("-" * len(_s), file = output_handle)

    def add_current_addr(self, addr: int, idx: int):

        new_addr_id = Stack_view.get_elem_id()
        self.addrs[idx].append(new_addr_id)
        Stack_view.stack_values[new_addr_id] = addr

    def duplicate(self, copy_stack_associated_values = True):

        cpy = ROP_chain_ARM64()

        cpy.b = self.b.copy()
        cpy.eq_g = [l.copy() for l in self.eq_g]

        cpy.valid_stack_access = self.valid_stack_access
        cpy.valid_jump = self.valid_jump

        cpy.end_sp_pos = self.end_sp_pos

        cpy.addrs = deepcopy(self.addrs)
        for i in range(self.get_gadget_cnt()):
            for j in range(len(cpy.addrs[i])):

                addr_val = Stack_view.stack_values[cpy.addrs[i][j]]
                addr_id_cpy = Stack_view.get_elem_id()
                Stack_view.stack_values[addr_id_cpy] = addr_val
                cpy.addrs[i][j] = addr_id_cpy

        return self._duplicate_stack(cpy, copy_stack_associated_values)

    def remove_stack_ids(self):

        for i in range(self.get_gadget_cnt()):
            for addr in self.addrs[i]:
                Stack_view.del_id(addr)

        for stack_elem in self.stack.elements:
            if stack_elem.type == "64b_stack_val":
                Stack_view.del_id(stack_elem.info["id"])

        self.b = None
        self.effects = None
        self.stack = None

        self.end_sp_pos = None

        self.valid_jump = None
        self.valid_stack_access = None

    def join(self, snd: ROP_chain_ARM64 | ROP_chain_ARM64) -> ROP_chain_ARM64:

        fst_cpy: ROP_chain_ARM64
        res_chain, fst_cpy, snd_cpy = ROP_chain_ARM64._join_ef_stk(self, snd)

        if res_chain is None:
            return None

        if type(snd) == ROP_chain_ARM64:

            res_chain.b = fst_cpy.b + snd_cpy.b

            res_chain.addrs = fst_cpy.addrs + snd_cpy.addrs
            res_chain.eq_g = fst_cpy.eq_g + snd_cpy.eq_g

        else:

            res_chain.b = fst_cpy.b
            res_chain.b.append(snd_cpy.b)
            res_chain.addrs = fst_cpy.addrs
            res_chain.addrs.append(snd_cpy.addrs)
            res_chain.eq_g = fst_cpy.eq_g
            res_chain.eq_g.append(snd_cpy.eq_g)

        return res_chain

    def _make_payload(self, max_stack_size: int, forbidden_bytes: List[bytes] = [],
                        addr_offset: int = 0, pad_byte = b'A', last_jump: bytes = b'\x00' * 8) -> bytes:

        if self.valid_jump is not True or self.valid_stack_access is not True:
            raise RuntimeError(f"cannot build payload for an invalid chain: {self}")

        # check if bytes contain any forbidden byte
        def _check_bytes(to_check: bytes):

            for b in to_check:
                if b in forbidden_bytes:
                    return False

            return True

        jmp_addrs = {}
        # jump addresses for each gadget's jump

        # yield every jump address combinations
        # (more of them might be needed due to forbidden bytes
        #   either in the addresses themselves, or in the resulted payload)
        def _get_jmp_addrs():

            jmp_addrs_ids = [jid for jid in jmp_addrs.keys()]
            for i in range(len(jmp_addrs_ids)):
                assert(i == jmp_addrs_ids[i] - 1)

            def _get_addrs(arr):

                if len(arr) == 0:
                    yield []

                else:

                    for addr in jmp_addrs[arr[0]]:
                        for suffix in _get_addrs(arr[1:]):

                            yield [addr] + suffix

            try:
                for y in _get_addrs(jmp_addrs_ids):
                    yield [None] + y    # jump ids begin from 1, list from 0,
                                        # so the first None is for index alignment

            except StopIteration:
                return None

        def _fix_jmp_addr(g: ROP_gadget_ARM64 | ROP_chain_ARM64, jmp_ef: Effect_ARM64,
                            jmp_addr: int, jmp_id: int):

            jmp_stack_ids = set()

            free_stack_elements = {}
            reg_in_elements = {}

            # in case of stack assignments, z3 solver is used
            z3_solver = z3.Solver()

            # function that folds over the ARITH expression tree
            # and updates the z3 solver
            _aux_id = 0
            def _convert_to_z3_expr(el: Structured_element_ARM64):

                nonlocal _aux_id

                if el.type == "64b_stack_val":

                    if el.info["id"] in jmp_stack_ids:
                        return z3.BitVec(f"stack{el.info['id']}", 64)

                    val = Stack_view.stack_values[el.info["id"]]
                    if val is None:
                        val = free_stack_elements[el.info["id"]]

                    conv_el = z3.BitVec(f"c{_aux_id}", 64)
                    _aux_id += 1
                    z3_solver.add(conv_el == val)

                    return conv_el

                else:

                    conv_el = z3.BitVec(f"c{_aux_id}", 64)
                    _aux_id += 1

                    if el.type == "ct_val":
                        z3_solver.add(conv_el == el.info["value"])

                    elif el.type == "reg_in":
                        z3_solver.add(conv_el == reg_in_elements[el.info["reg_name"]])

                    elif el.type == "neg":

                        t = _convert_to_z3_expr(el.info["term_1"])
                        z3_solver.add(conv_el == ~t)

                    elif el.is_op():

                        t1 = _convert_to_z3_expr(el.info["term_1"])
                        t2 = _convert_to_z3_expr(el.info["term_2"])

                        if el.type == "add":
                            z3_solver.add(conv_el == t1 + t2)

                        elif el.type == "sub":
                            z3_solver.add(conv_el == t1 - t2)

                        elif el.type == "and":
                            z3_solver.add(conv_el == t1 & t2)

                        elif el.type == "or":
                            z3_solver.add(conv_el == t1 | t2)

                        elif el.type == "xor":
                            z3_solver.add(conv_el == t1 ^ t2)

                        elif el.type == "mul":
                            z3_solver.add(conv_el == t1 * t2)

                        elif el.type == "lsh":
                            z3_solver.add(conv_el == t1 << t2)

                        elif el.type == "rsh":
                            z3_solver.add(conv_el == z3.LShR(t1, t2))

                    return conv_el

            def _check_stack_elements(el: Structured_element_ARM64):

                if el is None:
                    return False

                if el.type == "64b_stack_val":

                    val = Stack_view.stack_values[el.info["id"]]
                    jmp = Stack_view.related_jump[el.info["id"]]

                    if jmp is not None:

                        if jmp == jmp_id:

                            if val is None:

                                jmp_stack_ids.add(el.info["id"])
                                return True

                            else:
                                raise RuntimeError("Unexpected jump stack element != None when building payload")

                        return False

                    else:

                        if val is None:
                            free_stack_elements.update({el.info["id"]: None})

                        return False

                elif el.type == "reg_in":

                    reg_in_elements.update({el.info["reg_name"]: None})
                    return False

                elif el.type == "ct_val":
                    return False

                elif el.is_op():

                    checked_1 = _check_stack_elements(el.info["term_1"])
                    checked_2 = _check_stack_elements(el.info["term_2"])

                    return checked_1 or checked_2

                elif el.type == "deref":
                    raise Exception("deref found while matching arith")

                raise RuntimeError(f"unknown element type {el.type} when building payload")

            jmp_stack_elements_found = _check_stack_elements(jmp_ef.params[0])

            if jmp_stack_elements_found is False:
                raise RuntimeError("Could not find jump associated stack element when building payload")

            for _ in range(Effect_ARM64.ARITH_P_TEST_CNT):

                for reg_in in reg_in_elements.keys():
                    reg_in_elements[reg_in] = random.randint(0, 2 ** 64)

                for s_id in free_stack_elements.keys():
                    free_stack_elements[s_id] = random.randint(0, 2 ** 64)

                z3_expr = _convert_to_z3_expr(jmp_ef.params[0])
                z3_solver.add(z3_expr == jmp_addr)

            if z3_solver.check() == z3.sat:

                sm = z3_solver.model()
                for stack_elem_id in jmp_stack_ids:

                    z3_stack_elem = z3.BitVec(f"stack{stack_elem_id}", 64)
                    val = sm[z3_stack_elem]

                    if val is not None:
                        Stack_view.stack_values[stack_elem_id] = val.as_long()

                return True

            else:
                return False

        # fix jump addresses

        for ef in self.effects:
            if ef.type == "JUMP":

                jmp_id = ef.destination_element.info["value"]
                if jmp_id is None or jmp_id < 1:
                    raise RuntimeError(f"Unexpected jump id {jmp_id} when building payload")

                if jmp_id < len(self.addrs):
                    jmp_addrs[jmp_id] = [Stack_view.stack_values[addr] for addr in self.addrs[jmp_id]]

                elif jmp_id > len(self.addrs):
                    raise RuntimeError(f"Unexpected jump id {jmp_id}, len(self.addrs) == {len(self.addrs)}")

        jmp_addrs[len(self.addrs)] = [last_jump]

        # determine the entry address for this chain
        entry_addr = None
        for addr_ in self.addrs[0]:

            addr = Stack_view.stack_values[addr_] + addr_offset

            if _check_bytes(to_bytes(addr)) is True:
                entry_addr = addr
                break

        if entry_addr is None:
            return None, None

        payload = b''

        payload_ok = True
        for addrs in _get_jmp_addrs():

            payload = b''

            self_fixed, _ = self.duplicate()

            for ef in self_fixed.effects:
                if ef.type == "JUMP":

                    jmp_id = ef.destination_element.info["value"]
                    to_fix_addr = addrs[jmp_id] + addr_offset

                    if _check_bytes(to_bytes(to_fix_addr)) is False:
                        payload_ok = False
                        break

                    payload_ok = _fix_jmp_addr(self_fixed, ef, to_fix_addr, jmp_id)

            if payload_ok is False:
                self_fixed.remove_stack_ids()
                continue

            for max_s_idx in range(len(self_fixed.stack.elements) - 1, -1, -1):

                elem = self_fixed.stack.elements[max_s_idx]
                if elem.type == "64b_stack_val" and \
                    Stack_view.stack_values[elem.info["id"]] is not None:

                    break

            for idx, el in enumerate(self_fixed.stack.elements):

                if idx > max_s_idx:
                    break

                if el.type == "64b_stack_pad":
                    payload += pad_byte * 8

                elif el.type == "64b_stack_val":

                    val = Stack_view.stack_values[el.info["id"]]
                    if val is None:
                        val = pad_byte * 8
                    else:
                        val = to_bytes(val)

                        if _check_bytes(val) is False:
                            payload_ok = False
                            self_fixed.remove_stack_ids()
                            break

                    payload += val

                else:
                    raise RuntimeError(f"Unexpected stack element type {el.type} when building payload")

            if len(payload) > max_stack_size:
                payload_ok = False
                self_fixed.remove_stack_ids()
                break

            if payload_ok is True:
                break

        if payload_ok is False:
            return None, None

        if len(payload) % 8 != 0:
            raise RuntimeError(f"Payload is not 8-byte aligned (length: {len(payload)})")

        return payload, entry_addr

    # mostly for debugging purposes
    def __str__(self):
        return f"ROP chain with stack {self.stack}, addresses are {'TODO'}, effects {[str(ef) for ef in self.effects]}"

class ROP_searcher_ARM64:

    def __init__(self, filepath: str):

        def _find_endpoint_offsets():

            end_offsets = []

            for i in range(len(self.exec_bytes)):

                xc_offset, xc = self.exec_bytes[i]

                idx_ = 0

                self.capstone.skipdata = True
                disas = self.capstone.disasm(xc, 0)
                for instr in disas:

                    if instr.mnemonic in Platform.ARM64.ENDPOINTS:
                        end_offsets.append((i, xc_offset + idx_))

                    idx_ += 4

            self.capstone.skipdata = False
            return end_offsets

        self.exec_bytes = Elf_util(filepath).load_x_bytes()
        self.capstone = Cs(CS_ARCH_ARM64, CS_MODE_ARM)

        # constant, can be changed, but 3 is the maximum recommended value
        self.BRUTEFORCE_DEPTH = 2

        self.endpoint_offsets: List[Tuple[int, int]] = _find_endpoint_offsets()

        # NOTE: self.raw_gadgets, self.raw_effects_to_gadgets
        #       cand be both VALID AND INVALID

        self.raw_gadgets: Set[ROP_gadget_ARM64] = set()
        self.jumponly_gadget = [] # TODO for "costly" padding, use jump-only gadgets
        self.raw_effects_to_gadgets: Dict[str, Dict[str, List[ROP_chain_ARM64]]] = {ef_t: {reg: [] for reg in Platform.ARM64.SUPPORTED_REGS} for ef_t in ["LOAD_S", "LOAD_CT", "MOV_RR", "ARITH"]}

        # self.gadgets, self.effects_to_gadgets include only VALID gadgets / chains

        self.gadgets: Set[ROP_chain_ARM64] = set()
        # TODO maybe a list of "almost-jumponly" g/chs that are valid, for padding???
        self.effects_to_gadgets: Dict[str, Dict[str, List[ROP_chain_ARM64]]] = {ef_t: {reg: [] for reg in Platform.ARM64.SUPPORTED_REGS} for ef_t in ["LOAD_S", "LOAD_CT", "MOV_RR", "ARITH"]}

        # cached results of calling self.get_trans_reg_graph()
        self.trans_register_graph = None
        self.path_from = None

    def find_gadgets(self):

        # dict to help identify gadget duplicates
        # helps identify gadgets that differ only by stack padding or ignored instructions
        opstr_to_gadgets: Dict[str, Tuple[ROP_gadget_ARM64, bool]] = {}
        # helps identify gadgets that are identical
        bytes_to_gadgets: Dict[str, Tuple[ROP_gadget_ARM64, bool]] = {}

        def _get_opstr(b_instr: bytes):

            opstr = ''

            for instr in self.capstone.disasm(b_instr, 0):

                if (instr.mnemonic in Platform.ARM64.IGNORED_INSTR_MNEMONICS) or (instr.mnemonic == "nop"):
                    continue

                opstr += instr.mnemonic
                opstr += instr.op_str

            return opstr

        # auxiliary method to synchronize stack ids
        def _stack_id_sync(g: ROP_gadget_ARM64, eg: ROP_gadget_ARM64):

            s_g = g.stack.elements
            s_eg = eg.stack.elements

            i = 0
            j = 0
            while (i < len(s_g)) and (j < len(s_eg)):

                while (i < len(s_g)) and (s_g[i].type == "64b_stack_pad"):
                    i += 1

                while (j < len(s_eg)) and (s_eg[j].type == "64b_stack_pad"):
                    j += 1

                if i == len(s_g):
                    assert(j == len(s_eg))

                if (i < len(s_g)) and (j < len(s_eg)):

                    Stack_view.del_id(s_eg[j].info["id"])
                    s_eg[j] = s_g[i]

                    i += 1
                    j += 1

                if i == len(s_g):
                    assert(j == len(s_eg))

        # TODO initialize jumponly gadget

        # to check in constant time if the basse address
        # of a current gadget candidate actually steps over another gadget
        endpoint_onlyoffsets = set(r[1] for r in self.endpoint_offsets)

        for xc_index, endg_offset in self.endpoint_offsets:

            # TODO jumponly gadget

            # -4k                        0           +4
            # |........|........|...  ...|    jump    |
            # g                     endpoint off

            neg_offset = 4
            while (endg_offset - neg_offset >= 0) and ((endg_offset - neg_offset) not in endpoint_onlyoffsets):

                b_vaddr = self.exec_bytes[xc_index][0]
                b_instr = self.exec_bytes[xc_index][1][endg_offset - neg_offset - b_vaddr: endg_offset - b_vaddr + 4]

                stop = False

                if b_instr in bytes_to_gadgets.keys():

                    # if prev gadget is also valid
                    if bytes_to_gadgets[b_instr][1] is True:
                        bytes_to_gadgets[b_instr][0].add_current_addr(endg_offset - neg_offset)

                else:

                    opstr = _get_opstr(b_instr)
                    if (opstr not in opstr_to_gadgets.keys()) or (opstr_to_gadgets[opstr][1] is True):

                        disas_instr_generator = self.capstone.disasm(b_instr, endg_offset - neg_offset)
                        g, stop = self.create_gadget(disas_instr_generator, b_instr, endg_offset - neg_offset)

                    if (g is not None) and (len(g.effects) > 0):

                        bytes_to_gadgets.update({b_instr: (g, True)})

                        if opstr in opstr_to_gadgets.keys():
                            opstr_to_gadgets[opstr][0].eq_g.append(g)

                        else:
                            opstr_to_gadgets.update({opstr: (g, True)})

                            for ef in g.effects:

                                if ef.type != "JUMP" and ef.destination_element.info["reg_name"] != "sp":
                                    self.raw_effects_to_gadgets[ef.type][ef.destination_element.info["reg_name"]].append(g)

                            self.raw_gadgets.add(g)

                    else:
                        bytes_to_gadgets.update({b_instr: (None, False)})
                        opstr_to_gadgets.update({opstr: (None, False)})

                if stop is True:
                    break

                neg_offset += 4

        # for each g, synchronize stack ids between g and gadgets from g.eq_g
        # FIXME unnecessary on ARM64??
        for g in self.raw_gadgets:
            for eg in g.eq_g:
                _stack_id_sync(g, eg)

        # sorting by used stack size
        for ef in ["LOAD_S", "LOAD_CT", "MOV_RR", "ARITH"]:
            for reg in Platform.ARM64.SUPPORTED_REGS:
                self.raw_effects_to_gadgets[ef][reg].sort(key = lambda g: len(g.stack.elements))

    # main method of parsing instruction chunks and creating gadgets
    # here it is decided whether the gadget is valid / accepted / supported, what effects is has and so on
    # NOTE: addr parameter contains the default address, when ASLR/PIE is NOT enabled
    def create_gadget(self, instr_generator: Generator[CsInsn, None, None], b_instr: bytes, addr: int = None) -> Tuple[ROP_gadget_ARM64, bool]:

        # decide here whether to send signal to the caller procedure
        # so that it stops appending preffixes to the same "gadget"
        def _send_stop_flag():
            return len(b_instr) > ROP_gadget_ARM64.MAX_GADGET_BYTE_LEN

        def _gadget_end(instr: CsInsn):
            return instr.mnemonic in ["blr", "br", "ret"]

        # first, each instruction is analysed semantically and translated in some effects
        # then, the effects will be cumulated from first to last instruction, to obtain the gadget
        effects_per_instruction: List[List[Effect_ARM64]] = []

        # NOTE: om ARM64, stop flag can be set when
        #       an illegal / unknown instr is found, because
        #       the instructions are always 4 bytes
        #       and an unsupported 4 byte sequence
        #       cannot turn into something useful
        #       even if we add prefix bytes to it

        ends_correctly = False
        for instr in instr_generator:

            instr_effects = Effect_ARM64.analyse_instr(instr)
            if instr_effects is None:
                return None, True

            effects_per_instruction.append(instr_effects)

            if _gadget_end(instr) is True:
                ends_correctly = True
                break

        if ends_correctly is False:
            return None, True

        new_stack, new_effects, valid_stack_access, valid_jump, end_sp_pos, unres_derefs = \
            Effect_ARM64.join_instr_effects(effects_per_instruction)

        if new_stack is None:
            return None, _send_stop_flag()

        candidate_gadget = ROP_gadget_ARM64()
        candidate_gadget.stack = new_stack
        candidate_gadget.effects = new_effects

        candidate_gadget.valid_stack_access = valid_stack_access
        candidate_gadget.valid_jump = valid_jump

        candidate_gadget.end_sp_pos = end_sp_pos

        candidate_gadget.unresolved_derefs = unres_derefs

        candidate_gadget.add_current_addr(addr)
        candidate_gadget.b = b_instr

        return candidate_gadget, _send_stop_flag()

    # only for valid-related statistics
    def valid_stats(self):

        valid_jmp_cnt = 0
        valid_acc_cnt = 0
        valid_cnt = 0
        valid_only_jmp_cnt = 0
        valid_only_acc_cnt = 0
        full_invalid_cnt = 0

        for g in self.raw_gadgets:

            if g.valid_jump is True:
                valid_jmp_cnt += 1
            if g.valid_stack_access is True:
                valid_acc_cnt += 1
            if g.valid_jump is True and g.valid_stack_access is True:
                valid_cnt += 1
            if g.valid_jump is True and g.valid_stack_access is False:
                valid_only_jmp_cnt += 1
            if g.valid_jump is False and g.valid_stack_access is True:
                valid_only_acc_cnt += 1
            if g.valid_jump is False and g.valid_stack_access is False:
                full_invalid_cnt += 1

            assert(g.valid_jump in [True, False])
            assert(g.valid_stack_access in [True, False])
            assert(g.end_sp_pos % 2 == 0)

        assert(valid_only_acc_cnt + valid_only_jmp_cnt + \
                valid_cnt + full_invalid_cnt == len(self.raw_gadgets))

        print(" ----> stats from self.raw_gadgets:")
        print(f"\ntotal gadgets {len(self.raw_gadgets)}, full valid {valid_cnt}, " + \
                f"valid jump {valid_jmp_cnt}, " + \
                f"valid stack access {valid_acc_cnt}, " + \
                f"valid jump only {valid_only_jmp_cnt}, " + \
                f"valid stack access only {valid_only_acc_cnt}, " + \
                f"full invalid {valid_only_jmp_cnt}\n")

        for g in self.gadgets:
            assert(g.valid_jump is True)
            assert(g.valid_stack_access is True)

        print(" ----> stats from self.gadgets:")
        print(f"\nvalid gadgets (and chains): {len(self.gadgets)}\n")

    # method that filters the valid gadgets
    # and also tries to build valid chains from
    # invalid and valid gadgets
    def validate_raw_gadgets(self, q0 = 1, q1 = 1):

        # returns ok_to_advertise, contains_sp (both bool)
        # used for both stack access validation, and jump validation
        def _advertisement_check(elem: Structured_element_ARM64):

            if elem is None:
                return True, False

            if elem.type == "reg_in":

                if elem.info["reg_name"] == "sp":
                    return True, True

                return False, False

            if elem.type == "ct_val":
                return True, False

            if elem.type == "64b_stack_val":
                return True, False

            if elem.type == "deref":
                raise Exception("deref found in a supposedly valid gadget / chain")

            if elem.is_op():

                l1, l2 = _advertisement_check(elem.info["term_1"])
                r1, r2 = _advertisement_check(elem.info["term_2"])

                return l1 and r1, l2 or r2

            raise Exception(f"unexpected element {elem}")

        def _extract_stk_requests(elem: Structured_element_ARM64, regs: list, inside_deref: bool):

            if elem is None:
                return

            if elem.type == "reg_in" and elem.info["reg_name"] != "sp":

                if inside_deref is True and elem.info["reg_name"] not in regs:
                    regs.append(elem.info["reg_name"])

            elif elem.is_op():
                _extract_stk_requests(elem.info["term_1"], regs, inside_deref)
                _extract_stk_requests(elem.info["term_2"], regs, inside_deref)

            elif elem.type == "deref":

                # even if gadget is invalid, nested derefs are not expected
                # because the gadget has already been checked at join instr
                if inside_deref is True:
                    raise Exception("nested deref")

                _extract_stk_requests(elem.info["expr"], regs, True)

        def _extract_jmp_requests(elem: Structured_element_ARM64, regs: list):

            if elem is None:
                return

            if elem.type == "reg_in" and elem.info["reg_name"] not in regs:
                regs.append(elem.info["reg_name"])

            elif elem.is_op():
                _extract_jmp_requests(elem.info["term_1"], regs)
                _extract_jmp_requests(elem.info["term_2"], regs)

            elif elem.type == "deref":
                raise Exception("unexpected deref inside JUMP expression")

        # step 1)
        # filter out valid from invalid

        valid = []
        invalid = []
        for g in self.raw_gadgets:

            if g.valid_jump is True and g.valid_stack_access is True:
                valid.append(g)
            elif g.valid_stack_access is False:
                invalid.append(g)

        # step 2)
        # try to validate stack access

        valid_stack_acc_only = []
        # stack acc valid, jump invalid

        replace_reg_dict = {reg: [[], [], set(), set()] for reg in (Platform.ARM64.SUPPORTED_REGS + ["sp"])}
        # {reg: [
        #        [list of gadgets whose effect with dest elem == reg does not contain other reg_in arguments),
        #        [list of gadgets whose effect with dest elem == reg does not contain other reg_in arguments,
        #           but contain SP),
        #        (set of elements from the first list),
        #        (set of elements from the second list)
        #       ]
        # }
        # list is used to be able to execute random.choice()
        # set is used to be able to check for existence in constant time

        # NOTE that some gadgets / chains might actually require less registers to be replaced
        #       for example, Xt <- [SP + 8 + (reg & 0)] does not require reg to be replaced
        #       to have a valid stack access
        #       still, taking into account these kind of cases require greatly increasing the complexity
        #       of the code, for very little gain
        #       so this kind of "optimizations" are not implemented

        iv_to_requests = {iv: [] for iv in invalid}
        # {invalid gadget: [regs to replace]}

        iv_to_tested = {iv: set() for iv in invalid}
        # {invalid gadget: set((g0, g1, ...), ...)}
        # contains ordered join combinations for the current invalid gadget
        # that were previously tried

        # loop through invalid gadgets
        # and populate iv_to_requests
        for iv in invalid:

            for ef in iv.effects:
                if ef.type in ["ARITH", "JUMP", "LOAD_S"]:
                    _extract_stk_requests(ef.params[0], iv_to_requests[iv], False)

            for deref_el in iv.unresolved_derefs:
                _extract_stk_requests(deref_el.info["expr"], iv_to_requests[iv], True)

        # generator that tries to yield unbiased sequences
        # of valid gadgets (or chains) that (hopefully, will) satisfy requests
        # for a specific invalid gadget
        # (round robin + random and fisher-yates)
        # NOTE: currently, only one "sp" replacement is preferred
        #       this is done to avoid unnecessary checks for validity in situations
        #       like Xt <- [SP + SP << 8] or Xt <- [SP * SP] and so on
        def _serve_stk_request(iv: ROP_gadget_ARM64):

            reqs = iv_to_requests[iv]

            if len(reqs) == 0:
                while True:
                    yield []

            MAX_CONSECUTIVE_FAILS = 3
            consecutive_fails = 0

            while True:

                # select which reg is to be replaced with an expression containing sp

                sp_choice_idx = 0
                while len(replace_reg_dict[reqs[sp_choice_idx]][1]) == 0:

                    sp_choice_idx += 1
                    sp_choice_idx %= len(reqs)

                sp_choice_reg = reqs[sp_choice_idx]

                # select the rest of the replacements

                seq: List[ROP_chain_ARM64 | ROP_gadget_ARM64] = []
                seq.append(random.choice(replace_reg_dict[sp_choice_reg][1]))

                for req in reqs:

                    if req == sp_choice_reg:
                        continue

                    # len(seq) <= len(reqs) which is (very) small,
                    # so a for in a for is not a problem

                    already_chosen = False
                    for chosen in seq:

                        if chosen in replace_reg_dict[req][3] or \
                            chosen in replace_reg_dict[req][2]:

                            already_chosen = True
                            break

                    if already_chosen is True:
                        continue

                    if len(replace_reg_dict[req][0]) == 0:
                        seq.append(random.choice(replace_reg_dict[req][1]))
                    else:
                        seq.append(random.choice(replace_reg_dict[req][0]))

                # check stack size limits

                stack_size = 0
                for v in seq:
                    stack_size += v.get_stack_size()

                if stack_size > Effect_ARM64.VALIDATION_SEARCH_MAX_STACK_SIZE:

                    consecutive_fails += 1
                    if consecutive_fails > MAX_CONSECUTIVE_FAILS:
                        return None

                    continue

                # randomize joining order

                for i in range(len(seq) - 1):
                    j = random.randint(i, len(seq) - 1)

                    aux = seq[i]
                    seq[i] = seq[j]
                    seq[j] = aux

                # check if it has been tried before

                tseq = tuple(seq)
                if tseq in iv_to_tested[iv]:

                    consecutive_fails += 1
                    if consecutive_fails > MAX_CONSECUTIVE_FAILS:
                        return None

                    continue

                consecutive_fails = 0

                iv_to_tested[iv].add(tseq)
                yield seq

        serve_request = {iv: _serve_stk_request(iv) for iv in invalid}
        # dictionary that contains _serve_request() generators

        # yield (at most) q elements
        def serve_request_q(iv: ROP_gadget_ARM64, q):

            try:

                for _ in range(q):
                    yield next(serve_request[iv])

            except StopIteration:
                return None

        # the gs / chs in this set were already tested for advertisement
        # (only for optimization purposes)
        already_advertised = set()

        new_validated = 1
        while new_validated > 0:

            new_validated = 0

            # loop through valid gadgets / chains, to "advertise" their effects
            for v in valid:
                if v not in already_advertised:

                    for ef in v.effects:
                        if ef.type in ["LOAD_CT", "LOAD_S", "MOV_RR", "ARITH"]:

                            ok_to_advertise, contains_sp = _advertisement_check(ef.params[0])
                            if ok_to_advertise is True:

                                if contains_sp is False:
                                    replace_reg_dict[ef.destination_element.info["reg_name"]][0].append(v)
                                    replace_reg_dict[ef.destination_element.info["reg_name"]][2].add(v)
                                else:
                                    replace_reg_dict[ef.destination_element.info["reg_name"]][1].append(v)
                                    replace_reg_dict[ef.destination_element.info["reg_name"]][3].add(v)

                    already_advertised.add(v)

            # loop through invalid gadgets, try to join with valid gadgets / chains
            iv: ROP_gadget_ARM64
            validated_idx = set()
            for iv_idx, iv in enumerate(invalid):

                # check if registers can be replaced

                replaceable = True
                replaceable_w_sp = False

                reqs = iv_to_requests[iv]
                for req in reqs:

                    if len(replace_reg_dict[req][0]) == 0 and \
                        len(replace_reg_dict[req][1]) == 0:

                        replaceable = False
                        break

                    if len(replace_reg_dict[req][1]) > 0:
                        replaceable_w_sp = True

                if replaceable is False or \
                    replaceable_w_sp is False:

                    continue

                # try to replace regs

                seq: List[ROP_gadget_ARM64 | ROP_chain_ARM64]
                for seq in serve_request_q(iv, q0):

                    if seq is None:
                        break

                    if len(seq) == 0:
                        raise Exception("unexpected len(seq) == 0")

                    candidate_ch = ROP_chain_ARM64.convert(seq[0].duplicate()[0])
                    for v in seq[1:]:

                        candidate_ch_aux = candidate_ch.join(ROP_chain_ARM64.convert(v))

                        if candidate_ch_aux is None:
                            candidate_ch = None
                            break

                        candidate_ch.remove_stack_ids()
                        candidate_ch = candidate_ch_aux

                    if candidate_ch is None:
                        continue

                    iv_aux = ROP_chain_ARM64.convert(iv.duplicate()[0])
                    candidate_ch_aux = candidate_ch.join(iv_aux)

                    if candidate_ch_aux is not None:
                        candidate_ch.remove_stack_ids()

                    candidate_ch = candidate_ch_aux

                    if candidate_ch is None:
                        continue

                    # validity of stack access and jumps have already been recalculated
                    # inside joins

                    # outcome of validity results

                    if candidate_ch.valid_stack_access is True:

                        # assert(len(candidate_ch.unresolved_derefs) == 0)

                        if candidate_ch.valid_jump is True:

                            new_validated += 1
                            valid.append(candidate_ch)

                        elif candidate_ch.valid_jump is False:
                            valid_stack_acc_only.append(candidate_ch)

                        # we are satisfied with at least one validation
                        # per invalid gadget
                        # (to avoid an explosion in the number of new chains)
                        validated_idx.add(iv_idx)

                    else:
                        candidate_ch.remove_stack_ids()

            # eliminate invalid gadgets that participate in at least
            # one newly built valid chain
            old_invalid = invalid
            invalid = []

            for iv_idx in range(len(old_invalid)):

                if iv_idx not in validated_idx:
                    invalid.append(old_invalid[iv_idx])

        # step 3)
        # try to validate jumps
        # (analogous with step 2, but without treating sp separately)

        # actualize invalid (stack acc valid, jump invalid)
        invalid = valid_stack_acc_only
        for g in self.raw_gadgets:

            if g.valid_stack_access is True and g.valid_jump is False:
                invalid.append(g)

        replace_reg_dict = {reg: [[], set()] for reg in Platform.ARM64.SUPPORTED_REGS}
        # {reg: [
        #        [list of gadgets whose effect with dest elem == reg does not contain other reg_in arguments),
        #        (set of elements from the previous list)
        #       ]
        # }

        iv_to_requests = {iv: [] for iv in invalid}
        # {invalid gadget: [regs to replace]}

        iv_to_tested = {iv: set() for iv in invalid}
        # {invalid gadget: set((g0, g1, ...), ...)}
        # contains ordered join combinations for the current invalid gadget
        # that were previously tried

        # loop through invalid gadgets
        # and populate iv_to_requests
        for iv in invalid:

            for ef in iv.effects:
                if ef.type == "JUMP":
                    _extract_jmp_requests(ef.params[0], iv_to_requests[iv])

        # almost the same as the "validate stack access" version
        def _serve_jmp_request(iv: ROP_gadget_ARM64):

            reqs = iv_to_requests[iv]

            if len(reqs) == 0:
                while True:
                    yield []

            MAX_CONSECUTIVE_FAILS = 3
            consecutive_fails = 0

            while True:

                # select the replacements

                seq: List[ROP_chain_ARM64 | ROP_gadget_ARM64] = []
                for req in reqs:

                    already_chosen = False
                    for chosen in seq:

                        if chosen in replace_reg_dict[req][1]:

                            already_chosen = True
                            break

                    if already_chosen is True:
                        continue

                    seq.append(random.choice(replace_reg_dict[req][0]))

                # check stack size limits

                stack_size = 0
                for v in seq:
                    stack_size += v.get_stack_size()

                if stack_size > Effect_ARM64.VALIDATION_SEARCH_MAX_STACK_SIZE:

                    consecutive_fails += 1
                    if consecutive_fails > MAX_CONSECUTIVE_FAILS:
                        return None

                    continue

                # randomize joining order

                for i in range(len(seq) - 1):
                    j = random.randint(i, len(seq) - 1)

                    aux = seq[i]
                    seq[i] = seq[j]
                    seq[j] = aux

                # check if it has been tried before

                tseq = tuple(seq)
                if tseq in iv_to_tested[iv]:

                    consecutive_fails += 1
                    if consecutive_fails > MAX_CONSECUTIVE_FAILS:
                        return None

                    continue

                consecutive_fails = 0

                iv_to_tested[iv].add(tseq)
                yield seq

        serve_request = {iv: _serve_jmp_request(iv) for iv in invalid}

        already_advertised = set()

        new_validated = 1
        while new_validated > 0:

            new_validated = 0

            # loop through valid gadgets / chains, to "advertise" their effects
            for v in valid:
                if v not in already_advertised:

                    for ef in v.effects:
                        if ef.type in ["LOAD_CT", "LOAD_S", "MOV_RR", "ARITH"]:

                            ok_to_advertise, contains_sp = _advertisement_check(ef.params[0])
                            if ok_to_advertise is True and contains_sp is False:

                                replace_reg_dict[ef.destination_element.info["reg_name"]][0].append(v)
                                replace_reg_dict[ef.destination_element.info["reg_name"]][1].add(v)

                    already_advertised.add(v)

            # loop through invalid gadgets, try to join with valid gadgets / chains
            iv: ROP_gadget_ARM64
            validated_idx = set()
            for iv_idx, iv in enumerate(invalid):

                # check if registers can be replaced

                replaceable = True

                reqs = iv_to_requests[iv]

                if len(reqs) == 0:
                    raise Exception("unexpected len(reqs) == 0")

                # sp cannot be replaced by anothing else other than sp + offset
                if "sp" in reqs:
                    continue

                for req in reqs:

                    if len(replace_reg_dict[req][0]) == 0:

                        replaceable = False
                        break

                if replaceable is False:
                    continue

                # try to replace regs

                seq: List[ROP_gadget_ARM64 | ROP_chain_ARM64]
                for seq in serve_request_q(iv, q1):

                    if seq is None:
                        break

                    if len(seq) == 0:
                        raise Exception("unexpected len(seq) == 0")

                    candidate_ch = ROP_chain_ARM64.convert(seq[0].duplicate()[0])
                    for v in seq[1:]:

                        candidate_ch_aux = candidate_ch.join(ROP_chain_ARM64.convert(v))

                        if candidate_ch_aux is None:
                            candidate_ch = None
                            break

                        candidate_ch.remove_stack_ids()
                        candidate_ch = candidate_ch_aux

                    if candidate_ch is None:
                        continue

                    iv_aux = ROP_chain_ARM64.convert(iv.duplicate()[0])
                    candidate_ch_aux = candidate_ch.join(iv_aux)

                    if candidate_ch_aux is not None:
                        candidate_ch.remove_stack_ids()

                    candidate_ch = candidate_ch_aux

                    if candidate_ch is None:
                        continue

                    # outcome of validity results

                    if candidate_ch.valid_jump is True:

                        if candidate_ch.valid_stack_access is False:
                            raise Exception("unexpected only jump valid gadget")

                        # candidate_ch.show()

                        new_validated += 1
                        valid.append(candidate_ch)

                        # we are satisfied with at least one validation
                        # per invalid gadget
                        # (to avoid an explosion in the number of new chains)
                        validated_idx.add(iv_idx)

                    else:
                        candidate_ch.remove_stack_ids()

            # eliminate invalid gadgets that participate in at least
            # one newly built valid chain
            old_invalid = invalid
            invalid = []

            for iv_idx in range(len(old_invalid)):

                if iv_idx not in validated_idx:
                    invalid.append(old_invalid[iv_idx])

        # post-processing the valid gadgets
        # and some sanity checks

        # NOTE: self.gadgets also contains chains,
        #       but are considered "gadgets" in the sense that these chains
        #       are indivisible and (some of) their individual gadgets are invalid
        self.gadgets = set(valid)

        v: ROP_gadget_ARM64 | ROP_chain_ARM64
        for v in self.gadgets:

            if v.valid_jump is not True or v.valid_stack_access is not True:
                raise Exception(f"corrupted valid gadged / chain {v}")

            for ef in v.effects:

                if ef.type != "JUMP" and ef.destination_element.info["reg_name"] != "sp":
                    self.effects_to_gadgets[ef.type][ef.destination_element.info["reg_name"]].append(v)

                if ef.type == "LOAD_S" and ef.params[0].type != "64b_stack_val":

                    if ef.params[0].is_op():
                        ef.type = "ARITH"
                    else:
                        raise Exception(f"unexpected 'valid' LOAD_S effect: {ef}")

        # FIXME use g.get_stack_size() instead of len(g.stack.elements) ???
        # sorting by used stack size
        for ef in ["LOAD_S", "LOAD_CT", "MOV_RR", "ARITH"]:
            for reg in Platform.ARM64.SUPPORTED_REGS:
                self.effects_to_gadgets[ef][reg].sort(key = lambda g: len(g.stack.elements))

    # creates a graph for transitioning a value between registers
    # NOTE: it does NOT support register start values
    def get_trans_reg_graph(self):

        if self.trans_register_graph is not None:
            return self.trans_register_graph, self.path_from

        # dict to retain all gadgets for moving a value from a register to another
        trans_reg_graph: Dict[str, Dict[str, List[ROP_gadget_ARM64]]] = {dest: {src: [] for src in Platform.ARM64.SUPPORTED_REGS} for dest in Platform.ARM64.SUPPORTED_REGS}

        # whether there is a path from [reg_dest][reg_src] or not
        path_from: Dict[str, Dict[str, bool]] = {dest: {src: False for src in Platform.ARM64.SUPPORTED_REGS} for dest in Platform.ARM64.SUPPORTED_REGS}

        for dest in Platform.ARM64.SUPPORTED_REGS:
            for src in Platform.ARM64.SUPPORTED_REGS:

                if dest == src:
                    path_from[dest][src] = True
                    continue

                dest_from_src_ef = Effect_ARM64.make_mov_rr_effect(dest, src)
                dest_from_src_gadgets = self.search_gadget(wanted_effect=dest_from_src_ef, max_stack_size=2 ** 63,
                                                            reg_start_values=[], max_search_cnt=2 ** 63)

                if len(dest_from_src_gadgets) != 0:

                    trans_reg_graph[dest][src] = dest_from_src_gadgets
                    path_from[dest][src] = True

        # completing path_from (Roy-Warshal)
        for k in Platform.ARM64.SUPPORTED_REGS:
            for i in Platform.ARM64.SUPPORTED_REGS:
                for j in Platform.ARM64.SUPPORTED_REGS:

                    if (path_from[i][k] is True) and (path_from[k][j] is True):
                        path_from[i][j] = True

        self.trans_register_graph = trans_reg_graph
        self.path_from = path_from

        return trans_reg_graph, path_from

    # TODO add probabilistic selection instead of (almost) all possibilities
    # function that automatically finds a chain that satisfies the Effect_ARM64 R_dest <- R_src
    # based on max stack size and a trans graph
    # NOTE: inside this function, no other constraints or checks are implemented
    def transition_chain_generator(self, dest: str, src: str, trans_graph: Dict[str, Dict[str, List[ROP_gadget_ARM64]]], path_from: Dict[str, Dict[str, bool]], max_stack_size: int):

        # auxiliary data structure for the graph traversal
        _visited = set()

        # generator that returns a list of gadgets that compose the transfer path, and the accumulated stack size
        def _path_finder(current_reg: str, mss: int):

            _visited.add(current_reg)

            if current_reg == src:
                yield [], 0

            else:

                for src_reg, gs in trans_graph[current_reg].items():
                    if (src_reg not in _visited) and (path_from[src_reg][src] is True) and (len(gs) > 0):

                        mss_reached = False

                        for path_suffix, stack_size in _path_finder(src_reg, mss - gs[0].get_stack_size()):
                            for trans_g in gs:

                                tgss = trans_g.get_stack_size()

                                if tgss + stack_size <= mss:
                                    yield [trans_g] + path_suffix, tgss + stack_size
                                else:
                                    mss_reached = True
                                    break

                            # FIXME remove this if?
                            #       its assumption is wrong,
                            #       not sure if it affects the results by much, tho
                            if mss_reached is True:
                                break

            _visited.remove(current_reg)

        return _path_finder(dest, max_stack_size)

    # auxiliary method that checks
    # whether any "subchain" was previously yielded or not
    @staticmethod
    def _check_if_duplicate(gs: List[ROP_gadget_ARM64 | ROP_chain_ARM64], b_cache: Dict[bytes, bool]):

        def _chunks(l: List[ROP_gadget_ARM64 | ROP_chain_ARM64]):

            if len(l) == 0:
                return

            if len(l) == 1:
                yield l[0].get_bytes()
                return

            if len(l) == 2:
                yield l[0].get_bytes()
                yield l[1].get_bytes()
                return

            for i in range(1, len(l)):
                yield b''.join([g.get_bytes() for g in l[:i]])
            yield b''.join([g.get_bytes() for g in l[1:]])

            yield from _chunks(l[1:])

        for subchain_b in _chunks(gs):
            if (subchain_b in b_cache.keys()) and (b_cache[subchain_b] is True):
                return True

        return False

    # internal method for searching a gadget
    # receives the wanted Effect_ARM64, max stack size, the fixed registers list (optional) and the register start values (optional)
    # NOTE: it is a FUNCTION, NOT a GENERATOR
    # NOTE: fixed registers may actually be used, as long as at the end of the gadget they contain their initial value
    # NOTE: the arguments are assumed to be valid
    # NOTE: max stack size is measured internally as the number of 64bit elements (max stack size in bytes // 8)
    def search_gadget(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                        reg_start_values: List[Effect_ARM64] = [], max_search_cnt: int = 100) -> List[ROP_gadget_ARM64]:

        # as in duplicate method, represents a map
        # between the original gadget stack ids and the new gadget stack ids
        def _get_new_id(old_new_id: Dict[int, int], old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        # a gadget can be referenced multiple times in the effects_to_gadgets dictionary
        # (when a gadget has multiple effects)
        # so, once a gadget has been checked, there is no need to check it twice
        tried_gadget_cache = set()

        # main function to try a gadget
        def _try_gadget(candidate_g: ROP_gadget_ARM64, wanted_effect: Effect_ARM64, searched_effect_types: List[str]):

            if candidate_g in tried_gadget_cache:
                return None

            if candidate_g.get_stack_size() > max_stack_size:

                tried_gadget_cache.add(candidate_g)
                return None

            # copies that can be manipulated without changing the original objects
            candidate_g_cpy, org_to_fstid = candidate_g.duplicate(copy_stack_associated_values=True)
            wanted_effect_cpy: Effect_ARM64 = deepcopy(wanted_effect)

            start_values_cpy = deepcopy(reg_start_values)
            candidate_g_cpy.effects = Effect_ARM64.join_effects(start_values_cpy, candidate_g_cpy.effects)

            # every entry in effects_to_gadgets has a corresponding Effect_ARM64
            # it is not kept in the dict, but can be found
            # by searching in the gadget's Effect_ARM64 list, by the reg_out name
            candidate_effect = None
            for ef in candidate_g_cpy.effects:

                if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == wanted_effect_cpy.destination_element.info["reg_name"]:
                    candidate_effect = ef
                    break

            if candidate_effect.type not in searched_effect_types:

                tried_gadget_cache.add(candidate_g)
                candidate_g_cpy.remove_stack_ids()
                return None

            matched = wanted_effect_cpy.match(candidate_effect)
            if matched is False:

                tried_gadget_cache.add(candidate_g)
                candidate_g_cpy.remove_stack_ids()
                return None

            is_fixed = candidate_g_cpy.check_fixed_regs(fixed_reg_list)
            if is_fixed is False:

                tried_gadget_cache.add(candidate_g)
                candidate_g_cpy.remove_stack_ids()
                return None

            if len(reg_start_values) > 0:

                # gadget is accepted, a third copy is created from the original gadget
                # that is not simplified like the first gadget copy,
                # but does contain all the additional stack ids and their associated value from the first gadget copy
                # then, the first temporary copy has its stack ids and other contents removed

                accepted_gadget, org_to_sndid = candidate_g.duplicate(copy_stack_associated_values=True)

                for org_stack_elem in candidate_g.stack.elements:
                    if org_stack_elem.type == "64b_stack_val":

                        fstid = _get_new_id(org_to_fstid, org_stack_elem.info["id"])

                        if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[fstid]:
                            raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[fstid]}")

                        if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[fstid]:

                            if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                raise RuntimeError("original gadget and cloned gadget non-null values are different")

                            sndid = _get_new_id(org_to_sndid, org_stack_elem.info["id"])
                            Stack_view.stack_values[sndid] = Stack_view.stack_values[fstid]

                        # else, the accepted_gadget already has the original value

                candidate_g_cpy.remove_stack_ids()

            else:
                accepted_gadget = candidate_g_cpy

            tried_gadget_cache.add(candidate_g)
            return accepted_gadget

        # table of possible Effect_ARM64 types matching
        # there can be multiple searched effects because, given specific circumstances, some effects can change type
        # NOTE: LOAD_S is intentionally restricted to only other LOAD_S effects, for an efficient/ fast search
        #       if one wants to have all the possible ways of loading a value in a register, LOAD_CT matching should be chosen instead
        # LOAD_S -> LOAD_S
        # LOAD_CT -> LOAD_CT, LOAD_S (always false if max stack size == 0), ARITH
        # MOV_RR -> MOV_RR, ARITH
        # ARITH -> ARITH

        # searched Effect_ARM64 type filtering
        # is done in two places: here, less restrictive
        # and inside the try gadget function, more restrictive
        # this is because we still want the search to be optimised
        # but also we need to take into account that the reg start values
        # can change some Effect_ARM64 types into other types

        found_g: List[ROP_gadget_ARM64] = []
        searched_effect_types_snd: List[str] = []
        searched_effect_types_fst: List[str] = []

        if wanted_effect.type == "LOAD_S":

            searched_effect_types_snd.append("LOAD_S")

            searched_effect_types_fst.append("LOAD_S")

        elif wanted_effect.type == "LOAD_CT":

            searched_effect_types_snd.append("LOAD_S")
            searched_effect_types_snd.append("LOAD_CT")
            searched_effect_types_snd.append("ARITH")

            searched_effect_types_fst.append("LOAD_S")
            searched_effect_types_fst.append("LOAD_CT")
            searched_effect_types_fst.append("MOV_RR")
            searched_effect_types_fst.append("ARITH")

        elif wanted_effect.type == "MOV_RR":

            searched_effect_types_snd.append("MOV_RR")
            searched_effect_types_snd.append("ARITH")

            searched_effect_types_fst.append("MOV_RR")
            searched_effect_types_fst.append("ARITH")

        elif wanted_effect.type == "ARITH":

            searched_effect_types_snd.append("ARITH")

            searched_effect_types_fst.append("ARITH")
            searched_effect_types_fst.append("MOV_RR")

        for srch_t in searched_effect_types_fst:

            found_per_t_cnt = 0

            candidate_g: ROP_gadget_ARM64
            for candidate_g in self.effects_to_gadgets[srch_t][wanted_effect.destination_element.info["reg_name"]]:

                accepted_gadget = _try_gadget(candidate_g, wanted_effect, searched_effect_types_snd)
                if accepted_gadget is not None:

                    found_g.append(accepted_gadget)

                    found_per_t_cnt += 1
                    if found_per_t_cnt == max_search_cnt:
                        break

        found_g.sort(key = lambda g: g.get_stack_size())
        return found_g[:max_search_cnt]

    # method responsible for automatically constructing rop chains
    # for only one wanted Effect_ARM64
    # based on gadgets from effects_to_gadgets dict
    # and on the different methods implemented
    # NOTE: based on different searching methods called, reg start values might be ignored
    def _search_chain(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                        reg_start_values: List[Effect_ARM64] = [], only_gadgets = False) -> List[ROP_chain_ARM64]:

        if max_stack_size < 0:
            return

        # every sequence of bytes is analysed exactly once
        # also, if a chain is yielded,
        # then no chain with this current chain as a "subchain" will be analysed
        # (this mechanism avoids chains that satisfy the wanted Effect_ARM64, but have junk preffixes / suffixes)
        b_cache: Dict[bytes, bool] = {}

        yield from self._search_gadgets(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                    reg_start_values=reg_start_values, b_cache=b_cache)

        if only_gadgets is False:

            if wanted_effect.type == "LOAD_CT":

                yield from self._search_chain_by_substitution(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                                reg_start_values=reg_start_values, b_cache=b_cache)

                yield from self._search_by_bruteforce(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                        reg_start_values=reg_start_values, b_cache=b_cache)

            elif wanted_effect.type == "ARITH":

                yield from self._search_chain_by_substitution(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                                reg_start_values=reg_start_values, b_cache=b_cache)

                yield from self._search_chain_by_substitution_adv(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                                    reg_start_values=reg_start_values, b_cache=b_cache)

                yield from self._search_by_bruteforce(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                        reg_start_values=reg_start_values, b_cache=b_cache)

            elif wanted_effect.type == "MOV_RR":

                yield from self._search_mov_chains(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                    reg_start_values=reg_start_values, b_cache=b_cache)

                yield from self._search_by_bruteforce(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                                        reg_start_values=reg_start_values, b_cache=b_cache)

            else:
                raise RuntimeError(f"Unrecognised wanted Effect_ARM64 type when searching chains: {wanted_effect.type}")

    # search chains with multiple wanted effects
    def search_chain(self, wanted_effects: List[Effect_ARM64], max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                        reg_start_values: List[Effect_ARM64] = [], only_gadgets = False) -> List[ROP_chain_ARM64]:

        b_cache: Dict[bytes, bool] = {}
        def _get_bytes(chs: List[ROP_chain_ARM64]):

            b_repr = b''
            for ch in chs:
                b_repr += ch.get_bytes()

            return b_repr

        def _get_new_id(old_new_id: Dict[int, int], old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        r_to_wef: Dict[str, Effect_ARM64] = {}

        def _map_r_to_wef():

            for wef in wanted_effects:

                r = wef.destination_element.info["reg_name"]

                if r in r_to_wef.keys():
                    raise RuntimeError("same destination register found in more than one wanted Effect_ARM64")

                r_to_wef.update({r: wef})

        r_to_gen: Dict[str, Generator] = {}
        r_to_startgen: Dict[str, Generator] = {}

        r_gen_mss: Dict[str, List[int]] = {}
        r_startgen_mss: Dict[str, List[int]] = {}

        def _update_generator(r: str, start: bool):

            wef = r_to_wef[r]

            if start is True:

                if len(r_startgen_mss[r]) == 0:
                    return False

                mss = r_startgen_mss[r][0]
                r_startgen_mss[r] = r_startgen_mss[r][1:]

                r_to_startgen.update({r: self._search_chain(wanted_effect = wef, max_stack_size = mss, fixed_reg_list = fixed_reg_list,
                                                            reg_start_values = reg_start_values, only_gadgets = False)})
            else:

                if len(r_gen_mss[r]) == 0:
                    return False

                mss = r_gen_mss[r][0]
                r_gen_mss[r] = r_gen_mss[r][1:]

                r_to_gen.update({r: self._search_chain(wanted_effect = wef, max_stack_size = mss, fixed_reg_list = fixed_reg_list,
                                                            reg_start_values = [], only_gadgets = False)})

            return True

        chs: List[Tuple[ROP_chain_ARM64, Set[str], Set[str]]] = []
        startchs: List[Tuple[ROP_chain_ARM64, Set[str], Set[str]]] = []

        # cache of bytes of generated chains
        ch_b_cache: Set[bytes] = set()
        startch_b_cache: Set[bytes] = set()

        def _generate_chs(qty: int):

            def _get_v_i(ch: ROP_chain_ARM64, r: str):

                validate = {r}
                invalidate = set()

                for r_, wef_ in r_to_wef.items():
                    if r_ != r:

                        wef_cpy = deepcopy(wef_)
                        ch_cpy, ch_to_chcpy_ids = ch.duplicate(copy_stack_associated_values = True)

                        to_match_ef: Effect_ARM64 = None
                        for ef in ch_cpy.effects:

                            if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == r_:
                                to_match_ef = ef
                                break

                        if to_match_ef is None:
                            continue

                        if wef_cpy.match(to_match_ef) is False:
                            ch_cpy.remove_stack_ids()
                            invalidate.add(r_)
                            continue

                        for org_stack_elem in ch.stack.elements:
                            if org_stack_elem.type == "64b_stack_val":

                                cpyid = _get_new_id(ch_to_chcpy_ids, org_stack_elem.info["id"])

                                if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[cpyid]:
                                    raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[cpyid]}")

                                if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[cpyid]:

                                    if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                        raise RuntimeError("original chain and cloned chain non-null values are different")

                                    Stack_view.stack_values[org_stack_elem.info["id"]] = Stack_view.stack_values[cpyid]

                        ch_cpy.remove_stack_ids()
                        validate.add(r_)

                return validate, invalidate

            total_gen = 0

            for r, _ in r_to_wef.items():

                gen = r_to_gen[r]

                _gen_cnt = 0
                while _gen_cnt < qty:

                    ch: ROP_chain_ARM64
                    for ch in gen:

                        b = ch.get_bytes()
                        if b in ch_b_cache:
                            continue

                        ch_b_cache.add(b)

                        validate, invalidate = _get_v_i(ch, r)
                        chs.append((ch, validate, invalidate))

                        _gen_cnt += 1
                        if _gen_cnt == qty:
                            break

                    if _gen_cnt < qty:

                        success = _update_generator(r, False)
                        if success is False:
                            break

                startgen = r_to_startgen[r]

                _startgen_cnt = 0
                while _startgen_cnt < qty:

                    ch: ROP_chain_ARM64
                    for ch in startgen:

                        b = ch.get_bytes()
                        if b in startch_b_cache:
                            continue

                        startch_b_cache.add(b)

                        validate, invalidate = _get_v_i(ch, r)
                        startchs.append((ch, validate, invalidate))

                        _startgen_cnt += 1
                        if _startgen_cnt == qty:
                            break

                    if _startgen_cnt < qty:

                        success = _update_generator(r, False)
                        if success is False:
                            break

                total_gen += _startgen_cnt + _gen_cnt

            if total_gen == 0:
                return False

            return True

        # cache of a state (mv + ci)
        state_cache: Dict[str, int] = {}

        def _get_state_str(mv: Set[str], ci: Set[str]):

            mvl = [s for s in mv]
            cil = [s for s in ci]
            mvl.sort()
            cil.sort()

            return f"{''.join(mvl)}|{''.join(cil)}"

        def _path_search(mv: Set[str], ci: Set[str], mss: int, start: bool):

            if len(mv) == 0:
                yield [], 0
                return

            _chs = chs
            if start is True:
                _chs = startchs

            # append chains that satisfy a wanted Effect_ARM64 from mv
            for ch, validate, invalidate in _chs:

                # stack size
                ch_ss = ch.get_stack_size()
                if ch_ss > mss:
                    continue

                # ci inclusion
                if invalidate.issubset(ci) is False:
                    continue

                # usefullness
                m0 = validate.difference(mv)
                m1 = validate.intersection(mv)

                if len(m1) == 0:
                    continue

                new_mv = mv.difference(m1)
                new_ci = ci.difference(invalidate).union(m0)

                # already visited
                state_str = _get_state_str(new_mv, new_ci)
                if (state_str in state_cache.keys()) and (state_cache[state_str] >= mss):
                    continue

                if state_str in state_cache.keys():
                    state_cache[state_str] = mss
                else:
                    state_cache.update({state_str: mss})

                for suf, stack_size in _path_search(new_mv, new_ci, mss - ch_ss, False):
                    yield [ch] + suf, stack_size + ch_ss

            # append chains that "guard" some wanted effects
            for ch, validate, invalidate in _chs:

                # stack size
                ch_ss = ch.get_stack_size()
                if ch_ss > mss:
                    continue

                # ci inclusion
                if invalidate.issubset(ci) is False:
                    continue

                # usefullness
                m0 = validate.difference(mv)
                m1 = validate.intersection(mv)

                if len(m0) == 0:
                    continue

                new_mv = mv.difference(m1)
                new_ci = ci.difference(invalidate).union(m0)

                # already visited
                state_str = _get_state_str(new_mv, new_ci)
                if (state_str in state_cache.keys()) and (state_cache[state_str] >= mss):
                    continue

                if state_str in state_cache.keys():
                    state_cache[state_str] = mss
                else:
                    state_cache.update({state_str: mss})

                for suf, stack_size in _path_search(new_mv, new_ci, mss - ch_ss, False):
                    yield [ch] + suf, stack_size + ch_ss

        if len(wanted_effects) == 1:

            yield from self._search_chain(wanted_effect = wanted_effects[0], max_stack_size = max_stack_size, fixed_reg_list = fixed_reg_list,
                                            reg_start_values = reg_start_values, only_gadgets = only_gadgets)

        elif only_gadgets is True:

            for g in self._search_gadgets(wanted_effect = wanted_effects[0], max_stack_size = max_stack_size, fixed_reg_list = fixed_reg_list,
                                            reg_start_values = reg_start_values, b_cache = b_cache):

                g_cpy, org_to_cpyid = g.duplicate()
                start_values_cpy = deepcopy(reg_start_values)
                g_cpy.effects = Effect_ARM64.join_effects(start_values_cpy, g_cpy.effects)

                wef_sat = True
                for wef in wanted_effects[1:]:

                    wef_cpy = deepcopy(wef)

                    to_check_effect: Effect_ARM64 = None
                    for ef in g_cpy.effects:

                        if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == wef_cpy.destination_element.info["reg_name"]:
                            to_check_effect = ef
                            break

                    if to_check_effect is None:
                        wef_sat = False
                        break

                    if wef_cpy.match(to_check_effect) is False:
                        wef_sat = False
                        break

                    if g_cpy.check_fixed_regs(fixed_reg_list) is False:
                        wef_sat = False
                        break

                if wef_sat is False:
                    g_cpy.remove_stack_ids()
                    continue

                for org_stack_elem in g.stack.elements:
                    if org_stack_elem.type == "64b_stack_val":

                        cpyid = _get_new_id(org_to_cpyid, org_stack_elem.info["id"])

                        if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[cpyid]:
                            raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[cpyid]}")

                        if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[cpyid]:

                            if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                raise RuntimeError("original chain and cloned chain non-null values are different")

                            Stack_view.stack_values[org_stack_elem.info["id"]] == Stack_view.stack_values[cpyid]

                g_cpy.remove_stack_ids()

                yield g

        else:

            _map_r_to_wef()

            r_gen_mss: Dict[str, List[int]] = {r: [max_stack_size / len(wanted_effects), max_stack_size] for r in r_to_wef.keys()}
            r_startgen_mss: Dict[str, List[int]] = {r: [max_stack_size / len(wanted_effects), max_stack_size] for r in r_to_wef.keys()}

            for r, _ in r_to_wef.items():
                _update_generator(r, False)
                _update_generator(r, True)

            try:

                qty_gen = 1
                while True:

                    success = _generate_chs(qty_gen)
                    if success is False:
                        break

                    path: List[ROP_chain_ARM64]
                    for path, stack_size in _path_search(mv = {r for r in r_to_wef.keys()}, ci = set(), mss = max_stack_size, start = True):

                        if stack_size > max_stack_size:
                            raise RuntimeError("max stack size constraint violated")

                        b = _get_bytes(path)
                        if b in b_cache.keys():
                            continue

                        b_cache.update({b: False})

                        if ROP_searcher_ARM64._check_if_duplicate(path, b_cache) is True:
                            continue

                        acc_ch, _ = path[-1].duplicate()
                        for ch in path[-2::-1]:

                            ch_aux = acc_ch.join(ch)
                            acc_ch.remove_stack_ids()
                            acc_ch = ch_aux

                            if acc_ch is None:
                                break

                        if acc_ch is None:
                            continue

                        b_cache[b] = True
                        yield acc_ch

                    qty_gen *= 2

            finally:

                ch: ROP_chain_ARM64
                for ch, _, _ in chs:
                    ch.remove_stack_ids()

                for ch, _, _ in startchs:
                    ch.remove_stack_ids()

    # internal method that searches only gadgets,
    # and converts the results into ROP_chain_ARM64 objects
    def _search_gadgets(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                        reg_start_values: List[Effect_ARM64] = [], b_cache: Dict[bytes, bool] = {}):

        print("search gadgets")

        gadgets = self.search_gadget(wanted_effect=wanted_effect, max_stack_size=max_stack_size, fixed_reg_list=fixed_reg_list,
                                            reg_start_values=reg_start_values, max_search_cnt=2 ** 63)

        for g in gadgets:

            b = g.get_bytes()
            if b not in b_cache.keys():

                b_cache.update({b: True})
                yield ROP_chain_ARM64.convert(g)

    # try to create chains between nodes that have path_from == False
    def _search_arith_to_mov(self, q = 1):

        def _advertisement_check(elem: Structured_element_ARM64):

            if elem is None:
                return True

            if elem.type == "reg_in":
                return False

            if elem.type == "ct_val":
                return True

            if elem.type == "64b_stack_val":
                return True

            if elem.type == "deref":
                raise Exception("deref found in a supposedly valid gadget / chain")

            if elem.is_op():

                l1 = _advertisement_check(elem.info["term_1"])
                r1 = _advertisement_check(elem.info["term_2"])

                return l1 and r1

            raise Exception(f"unexpected element {elem}")

        def _extract_requests(elem: Structured_element_ARM64, regs: list):

            if elem is None:
                return

            if elem.type == "reg_in" and elem.info["reg_name"] not in regs and \
                elem.info["reg_name"] != "sp":

                regs.append(elem.info["reg_name"])

            elif elem.is_op():
                _extract_requests(elem.info["term_1"], regs)
                _extract_requests(elem.info["term_2"], regs)

            elif elem.type == "deref":
                raise Exception("unexpected deref inside JUMP expression")

        _, path_from = self.get_trans_reg_graph()

        to_check = [g for g in self.gadgets]
        new_gs = []

        replace_reg_dict = {reg: [[], set()] for reg in Platform.ARM64.SUPPORTED_REGS}
        # {reg: [
        #        [list of gadgets whose effect with dest elem == reg does not contain other reg_in arguments),
        #        (set of elements from the first list),
        #       ]
        # }
        # list is used to be able to execute random.choice()
        # set is used to be able to check for existence in constant time

        g_to_requests = {g: [] for g in to_check}
        # {g: [[regs to replace for first arith effect in g.effects], ...]}

        g_to_tested = {g: set() for g in to_check}
        # {gadget: set((g0, g1, ...), ...)}
        # contains ordered join combinations for the current gadget
        # that were previously tried

        efidx_to_realidx = {g: [] for g in to_check}
        # see usage

        to_spare_regs = {g: [] for g in to_check}
        # see usage

        # loop through invalid gadgets
        # and populate g_to_requests
        for g in to_check:
            for realidx, ef in enumerate(g.effects):

                if ef.type == "ARITH":

                    reg_dest = ef.destination_element.info["reg_name"]

                    if reg_dest == "sp":
                        continue

                    g_to_requests[g].append([])
                    efidx_to_realidx[g].append(realidx)

                    _extract_requests(ef.params[0], g_to_requests[g][-1])

                    to_spare_regs[g].append([])

                    for reg_src in g_to_requests[g][-1]:
                        if path_from[reg_dest][reg_src] is False:

                            to_spare_regs[g][-1].append(reg_src)

        def _serve_request(g: ROP_gadget_ARM64, ef_idx: int):

            reqs = g_to_requests[g][ef_idx]
            to_spare = to_spare_regs[g][ef_idx]

            if len(reqs) == 0:
                while True:
                    yield []

            MAX_CONSECUTIVE_FAILS = 3
            consecutive_fails = 0

            to_spare_idx = 0

            while True:

                to_spare_reg = to_spare[to_spare_idx]
                to_spare_idx += 1
                to_spare_idx %= len(to_spare)

                # select the replacements

                seq: List[ROP_chain_ARM64 | ROP_gadget_ARM64] = []
                for req in reqs:
                    if req != to_spare_reg:

                        already_chosen = False
                        for chosen in seq:

                            if chosen in replace_reg_dict[req][1]:

                                already_chosen = True
                                break

                        if already_chosen is True:
                            continue

                        seq.append(random.choice(replace_reg_dict[req][0]))

                # check stack size limits

                stack_size = 0
                for v in seq:
                    stack_size += v.get_stack_size()

                if stack_size > Effect_ARM64.VALIDATION_SEARCH_MAX_STACK_SIZE:

                    consecutive_fails += 1
                    if consecutive_fails > MAX_CONSECUTIVE_FAILS:
                        return None

                    continue

                # randomize joining order

                for i in range(len(seq) - 1):
                    j = random.randint(i, len(seq) - 1)

                    aux = seq[i]
                    seq[i] = seq[j]
                    seq[j] = aux

                # check if it has been tried before

                tseq = tuple(seq)
                if tseq in g_to_tested[g]:

                    consecutive_fails += 1
                    if consecutive_fails > MAX_CONSECUTIVE_FAILS:
                        return None

                    continue

                consecutive_fails = 0

                g_to_tested[g].add(tseq)
                yield to_spare_reg, seq

        serve_request = {g: {ef_idx: _serve_request(g, ef_idx) for ef_idx in range(len(g_to_requests[g]))} for g in to_check}
        # dictionary that contains _serve_request() generators

        # yield (at most) q elements
        def serve_request_q(g: ROP_gadget_ARM64, ef_idx: int, q):

            try:

                for _ in range(q):
                    yield next(serve_request[g][ef_idx])

            except StopIteration:
                return None

        # the gs / chs in this set were already tested for advertisement
        # (only for optimization purposes)
        already_advertised = set()

        new_generated = 1
        while new_generated > 0:

            new_generated = 0

            # loop through valid gadgets / chains, to "advertise" their effects
            for g in self.gadgets:
                if g not in already_advertised:

                    for ef in g.effects:
                        if ef.type in ["LOAD_CT", "LOAD_S", "MOV_RR", "ARITH"]:

                            ok_to_advertise = _advertisement_check(ef.params[0])
                            if ok_to_advertise is True:

                                replace_reg_dict[ef.destination_element.info["reg_name"]][0].append(g)
                                replace_reg_dict[ef.destination_element.info["reg_name"]][1].add(g)

                    already_advertised.add(g)

            rem_idx = set()

            g: ROP_gadget_ARM64
            for g_idx, g in enumerate(to_check):

                satisf = 0
                for ef_idx in range(len(g_to_requests[g])):

                    # check if registers can be replaced

                    if len(to_spare_regs[g][ef_idx]) == 0:
                        continue

                    replaceable = True

                    reqs = g_to_requests[g][ef_idx]

                    # at least one reg kept, the others are removed
                    if len(reqs) <= 1:
                        continue

                    # sp cannot be replaced by anothing else other than sp + offset
                    if "sp" in reqs:
                        continue

                    for req in reqs:

                        if len(replace_reg_dict[req][0]) == 0:

                            replaceable = False
                            break

                    if replaceable is False:
                        continue

                    # try to replace regs

                    seq: List[ROP_gadget_ARM64 | ROP_chain_ARM64]
                    for spared_reg, seq in serve_request_q(g, ef_idx, q):

                        if seq is None:
                            break

                        if len(seq) == 0:
                            raise Exception("unexpected len(seq) == 0")

                        candidate_ch = ROP_chain_ARM64.convert(seq[0].duplicate()[0])
                        for ch in seq[1:]:

                            candidate_ch_aux = candidate_ch.join(ROP_chain_ARM64.convert(ch))

                            if candidate_ch_aux is None:
                                candidate_ch = None
                                break

                            candidate_ch.remove_stack_ids()
                            candidate_ch = candidate_ch_aux

                        if candidate_ch is None:
                            continue

                        g_aux = ROP_chain_ARM64.convert(g.duplicate()[0])
                        candidate_ch_aux = candidate_ch.join(g_aux)

                        if candidate_ch_aux is not None:
                            candidate_ch.remove_stack_ids()

                        candidate_ch = candidate_ch_aux

                        if candidate_ch is None:
                            continue

                        # outcome

                        reg_dest = g.effects[efidx_to_realidx[g][ef_idx]].destination_element.info["reg_name"]

                        mov_effect = Effect_ARM64.make_mov_rr_effect(reg_dest, spared_reg)

                        to_check_effect = None
                        for ef_ in candidate_ch.effects:

                            if ef_.type != "JUMP" and ef_.destination_element.info["reg_name"] == reg_dest:
                                to_check_effect = ef_
                                break

                        assert(to_check_effect.destination_element.info["reg_name"] == reg_dest)

                        if to_check_effect is None:
                            continue

                        if mov_effect.match(to_check_effect) is False:

                            candidate_ch.remove_stack_ids()
                            continue

                        print(reg_dest, spared_reg)

                        satisf += 1
                        new_generated += 1

                        new_gs.append(candidate_ch)

                if satisf > 0:
                    rem_idx.add(g_idx)

            # eliminate invalid gadgets that participate in at least
            # one newly built valid chain
            old_to_check = to_check
            to_check = []

            for g_idx in range(len(old_to_check)):

                if g_idx not in rem_idx:
                    to_check.append(old_to_check[g_idx])

        for g in new_gs:

            assert(g.valid_jump is True)
            assert(g.valid_stack_access is True)

            self.gadgets.add(g)

            for ef in g.effects:
                if ef.type != "JUMP" and ef.destination_element.info["reg_name"] != "sp":
                    self.effects_to_gadgets[ef.type][ef.destination_element.info["reg_name"]].append(g)

        # reset trans reg graph cache

        self.path_from = None
        self.trans_register_graph = None
        self.get_trans_reg_graph()

    # internal method that searches rop chains for MOV_RR type Effect_ARM64
    # resembles the _search_chain_by_substitution
    def _search_mov_chains(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                            reg_start_values: List[Effect_ARM64] = [], b_cache: Dict[bytes, bool] = {}):

        print("search mov chains")

        def _get_new_id(old_new_id: Dict[int, int], old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        trans_graph, path_from = self.get_trans_reg_graph()

        dest = wanted_effect.destination_element.info["reg_name"]
        src = wanted_effect.params[0].info["reg_name"]

        try:

            for path, _ in self.transition_chain_generator(dest, src, trans_graph, path_from, max_stack_size):

                if len(path) == 0:
                    continue

                if ROP_searcher_ARM64._check_if_duplicate(path, b_cache) is True:
                    continue

                candidate_ch = ROP_chain_ARM64.convert(path[-1].duplicate()[0])
                for g in path[-2::-1]:

                    candidate_ch_aux = candidate_ch.join(g)
                    candidate_ch.remove_stack_ids()
                    candidate_ch = candidate_ch_aux

                    if candidate_ch is None:
                        break

                if candidate_ch is None:
                    continue

                b = candidate_ch.get_bytes()
                if b in b_cache.keys():
                    candidate_ch.remove_stack_ids()
                    continue

                b_cache.update({b: False})

                candidate_ch_cpy, org_to_fstid = candidate_ch.duplicate()

                start_values_cpy = deepcopy(reg_start_values)
                candidate_ch_cpy.effects = Effect_ARM64.join_effects(start_values_cpy, candidate_ch_cpy.effects)

                # because of start values, the wanted effect needs to be checked
                wanted_effect_cpy = deepcopy(wanted_effect)

                to_check_ef: Effect_ARM64 = None
                for ef in candidate_ch_cpy.effects:

                    if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == dest:
                        to_check_ef = ef
                        break

                if (to_check_ef is None) or (wanted_effect_cpy.match(to_check_ef) is False):
                    candidate_ch_cpy.remove_stack_ids()
                    candidate_ch.remove_stack_ids()
                    continue

                if candidate_ch_cpy.check_fixed_regs(fixed_reg_list) is False:
                    candidate_ch_cpy.remove_stack_ids()
                    candidate_ch.remove_stack_ids()
                    continue

                for org_stack_elem in candidate_ch.stack.elements:
                    if org_stack_elem.type == "64b_stack_val":

                        fstid = _get_new_id(org_to_fstid, org_stack_elem.info["id"])

                        if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[fstid]:
                            raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[fstid]}")

                        if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[fstid]:

                            if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                raise RuntimeError("original chain and cloned chain non-null values are different")

                            Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[fstid]

                candidate_ch_cpy.remove_stack_ids()

                b_cache[b] = True
                yield candidate_ch

        finally:
            pass

            # trans graph items are no longer local

            '''for _, d in trans_graph.items():
                for _, gs in d.items():
                    if gs is not None:
                        for g in gs:
                            g.remove_stack_ids()'''

    def _search_chain_by_substitution(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                                        reg_start_values: List[Effect_ARM64] = [], b_cache: Dict[bytes, bool] = {}):

        if wanted_effect.type not in ["ARITH", "LOAD_CT"]:
            raise RuntimeError(f"tyring to search chain inside _search_chain_by_substitution for wanted Effect_ARM64 type {wanted_effect.type}")

        print("search by subst")

        def _get_bytes(chs: List[ROP_chain_ARM64]):

            b_repr = b''
            for ch in chs:
                b_repr += ch.get_bytes()

            return b_repr

        # graph as a list(dict) of neighbours, and the path existence matrix
        trans_graph, path_from = self.get_trans_reg_graph()

        # auxiliary method
        def _get_new_id(old_new_id: Dict[int, int], old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        # returns a list of all reg_in used, and the reg_out,
        # for an ARITH / LOAD_CT Effect_ARM64
        def _find_used_regs(ef: Effect_ARM64) -> Tuple[List[str], str]:

            def _rec_find(el: Structured_element_ARM64):

                if el is None:
                    return []

                if el.type == "reg_in":
                    return [el.info["reg_name"]]

                if el.is_op():
                    return _rec_find(el.info["term_1"]) + _rec_find(el.info["term_2"])

                if el.type == "64b_stack_val":
                    raise RuntimeError("wanted Effect_ARM64 contains stack elements")

                return []

            if ef.type == "LOAD_CT":
                return [], ef.destination_element.info["reg_name"]

            elif ef.type == "ARITH":

                found = _rec_find(ef.params[0])

                to_return = []
                for r in found:
                    if r not in to_return:
                        to_return.append(r)

                return to_return, ef.destination_element.info["reg_name"]

        # generator that computes all the possible
        # value transitions for the given registers, according to trans graph
        # (including identity replacements)
        # NOTE: a reg from regs_to_replace is a fixed source, and the outputs contain all possible destinations
        # NOTE: it assumes there is no stack element
        def _replace_seq_gen(regs_to_replace: List[str]):

            if len(regs_to_replace) == 0:
                yield []

            else:
                current_reg = regs_to_replace[0]
                for suffix in _replace_seq_gen(regs_to_replace[1:]):

                    for dest in path_from.keys():
                        if (path_from[dest][current_reg] is True) and (dest not in suffix):

                            yield [dest] + suffix

        # generator that returns all nodes that can reach dest_reg
        # NOTE: the dest_reg is a fixed destination, and the results contain all possible sources
        def _replace_dest_reg(dest_reg: str):

            for src, is_path in path_from[dest_reg].items():
                if is_path is True:
                    yield src

        # creates a new copy of the given Effect_ARM64,
        # and replaces all reg_in elements and, separately, the destination register
        # NOTE: it assumes there is no stack element
        def _apply_substitutions(ef: Effect_ARM64, regs_to_replace: List[str], replacements: List[str], dest_reg_replacement: str):

            def _rec_subst(el: Structured_element_ARM64):

                if el is None:
                    return None

                if el.type == "reg_in":
                    el.info["reg_name"] = replacements[regs_to_replace.index(el.info["reg_name"])]

                elif el.is_op():
                    _rec_subst(el.info["term_1"])
                    _rec_subst(el.info["term_2"])

                elif el.type == "64b_stack_val":
                    raise RuntimeError("wanted Effect_ARM64 contains stack elements")

            subst_ef = deepcopy(ef)
            subst_ef.destination_element.info["reg_name"] = dest_reg_replacement

            if subst_ef.type == "ARITH":
                _rec_subst(subst_ef.params[0])

            return subst_ef

        # replace every Rk[i] with the corresponding Rx[i]
        # the rx should be part of the result yielded by replace seq gen called on rk
        # mss - max stack size
        def _genpath(rk: List[str], rx: List[str], mss: int):

            def _perm(n, k):

                if k == 0:
                    yield []

                else:
                    for suf in _perm(n, k - 1):
                        for i in range(n):
                            if i not in suf:
                                yield [i] + suf

            def _internal_genpath(_rk, _rx, _mss):

                if len(_rk) == 0:
                    yield [], 0

                else:
                    rk_current = _rk[0]
                    rx_current = _rx[0]

                    for trans_gs, stack_size in self.transition_chain_generator(rx_current, rk_current, trans_graph, path_from, _mss):
                        for suffix, suf_stack_size in _internal_genpath(_rk[1:], _rx[1:], _mss - stack_size):

                            yield trans_gs + suffix, suf_stack_size + stack_size

            for p in _perm(len(rk), len(rk)):

                shuffled_rk = [rk[i] for i in p]
                shuffled_rx = [rx[i] for i in p]

                for path, stack_size in _internal_genpath(shuffled_rk, shuffled_rx, mss):
                    yield path, stack_size

        try:

            wef_used_regs, dest_reg = _find_used_regs(wanted_effect)

            for repl_seq in _replace_seq_gen(wef_used_regs):
                for repl_dest_reg in _replace_dest_reg(dest_reg):

                    wef_subst = _apply_substitutions(wanted_effect, regs_to_replace=wef_used_regs,
                                                        replacements=repl_seq, dest_reg_replacement=repl_dest_reg)

                    gs = self.search_gadget(wef_subst, max_stack_size=max_stack_size, fixed_reg_list=[],
                                            reg_start_values=[], max_search_cnt=2 ** 63)

                    if len(gs) == 0:
                        continue

                    try:

                        # for each gadget that satisfies wanted Effect_ARM64 with substitutions
                        #   for each way to move the result from intermediary dest reg to the real dest reg
                        #       for each way to replace all the reg_in elements
                        for wef_g in gs:

                            mss_aux1 = max_stack_size - wef_g.get_stack_size()
                            for repl_dest_path, ss_1 in self.transition_chain_generator(dest_reg, repl_dest_reg, trans_graph, path_from, mss_aux1):

                                mss_aux2 = mss_aux1 - ss_1
                                if mss_aux2 < 0:
                                    raise RuntimeError(f"stack size bigger than mss in genpath: {mss_aux1}, max {ss_1}")

                                for path, ss_2 in _genpath(wef_used_regs, repl_seq, mss_aux2):

                                    if ss_2 > mss_aux2:
                                        raise RuntimeError(f"stack size bigger than mss in genpath: {ss_2}, max {mss_aux2}")

                                    # join from the end to the beginning: repl_dest_path, g, path
                                    final_path: List[ROP_gadget_ARM64]
                                    final_path = repl_dest_path + [wef_g] + path

                                    if ROP_searcher_ARM64._check_if_duplicate(path, b_cache) is True:
                                        continue

                                    candidate_ch = ROP_chain_ARM64.convert(final_path[-1].duplicate()[0])
                                    for g_aux in final_path[-2::-1]:

                                        candidate_ch_aux = candidate_ch.join(g_aux)
                                        candidate_ch.remove_stack_ids()
                                        candidate_ch = candidate_ch_aux

                                        if candidate_ch is None:
                                            break

                                    if candidate_ch is None:
                                        b_cache.update({_get_bytes(repl_dest_path + [wef_g] + path): False})
                                        continue

                                    if candidate_ch.get_stack_size() != ss_1 + ss_2 + wef_g.get_stack_size():
                                        raise RuntimeError(f"stack size inconsistent: expected {ss_1 + ss_2 + wef_g.get_stack_size()}, got {candidate_ch.get_stack_size()}")

                                    b = candidate_ch.get_bytes()
                                    if b in b_cache.keys():
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    b_cache.update({b: False})

                                    # check if the obtained result really matches the wanted Effect_ARM64
                                    # (check is needed because the transition paths' side effects are not taken into account when searching)

                                    # also, the start values are plugged in
                                    candidate_ch_cpy, org_to_fstid = candidate_ch.duplicate()

                                    start_values_cpy = deepcopy(reg_start_values)
                                    candidate_ch_cpy.effects = Effect_ARM64.join_effects(start_values_cpy, candidate_ch_cpy.effects)

                                    to_check_effect: Effect_ARM64 = None
                                    for ef in candidate_ch_cpy.effects:

                                        if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == dest_reg:
                                            to_check_effect = ef
                                            break

                                    if to_check_effect is None:
                                        candidate_ch_cpy.remove_stack_ids()
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    wanted_effect_cpy = deepcopy(wanted_effect)

                                    if wanted_effect_cpy.match(to_check_effect) is False:
                                        candidate_ch_cpy.remove_stack_ids()
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    if candidate_ch_cpy.check_fixed_regs(fixed_reg_list) is False:
                                        candidate_ch_cpy.remove_stack_ids()
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    # match arith might have changed some stack values
                                    accepted_chain, org_to_sndid = candidate_ch.duplicate(copy_stack_associated_values=True)

                                    for org_stack_elem in candidate_ch.stack.elements:
                                        if org_stack_elem.type == "64b_stack_val":

                                            fstid = _get_new_id(org_to_fstid, org_stack_elem.info["id"])

                                            if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[fstid]:
                                                raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[fstid]}")

                                            if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[fstid]:

                                                if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                                    raise RuntimeError("original chain and cloned chain non-null values are different")

                                                sndid = _get_new_id(org_to_sndid, org_stack_elem.info["id"])
                                                Stack_view.stack_values[sndid] = Stack_view.stack_values[fstid]

                                    candidate_ch_cpy.remove_stack_ids()
                                    candidate_ch.remove_stack_ids()

                                    b_cache[b] = True
                                    yield accepted_chain

                    finally:

                        g: ROP_gadget_ARM64
                        for g in gs:
                            g.remove_stack_ids()

        finally:
            pass

            # trans graph items are no longer local

            # clean up unused stack ids
            '''for _, d in trans_graph.items():
                for _, gs in d.items():
                    if gs is not None:
                        for g in gs:
                            g.remove_stack_ids()'''

    # exactly like the above method
    # but also tries to substitute constants
    def _search_chain_by_substitution_adv(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                                        reg_start_values: List[Effect_ARM64] = [], b_cache: Dict[bytes, bool] = {}):

        print("search by sustitution advanced")

        if wanted_effect.type not in ["ARITH", "LOAD_CT"]:
            raise RuntimeError(f"tyring to search chain inside _search_chain_by_substitution for wanted Effect_ARM64 type {wanted_effect.type}")

        def _get_bytes(chs: List[ROP_chain_ARM64]):

            b_repr = b''
            for ch in chs:
                b_repr += ch.get_bytes()

            return b_repr

        # graph as a list(dict) of neighbours, and the path existence matrix
        trans_graph, path_from = self.get_trans_reg_graph()

        # gadget {Ri: {CTj: [gk, ...]}, ...} that stores gadgets of type gk: Ri <- CTj
        # almost the same as ROP_searcher_ARM64.effects_to_gadgets
        load_ct_gadgets_cache: Dict[str, Dict[int, List[ROP_gadget_ARM64]]] = {r: {} for r in Platform.ARM64.SUPPORTED_REGS}

        # auxiliary method that helps with substituting constants
        def _init_ct_g_cache():

            for r in Platform.ARM64.SUPPORTED_REGS:
                for g in self.effects_to_gadgets["LOAD_CT"][r]:

                    for ef in g.effects:
                        if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == r:

                            ct = ef.params[0].info["value"]

                            if ct in load_ct_gadgets_cache[r].keys():
                                load_ct_gadgets_cache[r][ct].append(g)
                            else:
                                load_ct_gadgets_cache[r].update({ct: [g]})

        # auxiliary method
        def _get_new_id(old_new_id: Dict[int, int], old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        # returns a list of all reg_in and constants used, and the reg_out
        # the registers or constants that can(will) be substituted in this function
        # are simply called symbols
        def _find_subst_symbols(ef: Effect_ARM64) -> Tuple[List[str | int], str]:

            def _rec_find(el: Structured_element_ARM64):

                if el is None:
                    return []

                if el.type == "reg_in":
                    return [el.info["reg_name"]]

                if el.type == "ct_val":
                    return [el.info["value"]]

                if el.is_op():
                    return _rec_find(el.info["term_1"]) + _rec_find(el.info["term_2"])

                if el.type == "64b_stack_val":
                    raise RuntimeError("wanted Effect_ARM64 contains stack elements")

                return []

            if ef.type == "LOAD_CT":
                return [], ef.destination_element.info["reg_name"]

            elif ef.type == "ARITH":

                found = _rec_find(ef.params[0])

                to_return = []
                for sym in found:
                    if sym not in to_return:
                        to_return.append(sym)

                return to_return, ef.destination_element.info["reg_name"]

        # generator that computes all the possible
        # value transitions for the given registers, according to trans graph
        # and all possible constant substitution with registers, also according to trans graph
        # but also according to load_ct_gadgets_cache
        # it includes identity replacements, both for registers and constants (the case a constant is NOT replaced is marked with None)
        # NOTE: a reg from syms_to_replace is a fixed source, and the outputs contain all possible destinations
        # NOTE: it assumes there is no stack element
        def _replace_seq_gen(syms_to_replace: List[str | int]):

            if len(syms_to_replace) == 0:
                yield []

            else:
                current_sym = syms_to_replace[0]
                for suffix in _replace_seq_gen(syms_to_replace[1:]):

                    # it is a register to be substituted
                    if current_sym in Platform.ARM64.SUPPORTED_REGS:

                        for dest in path_from.keys():
                            if (path_from[dest][current_sym] is True) and (dest not in suffix):

                                yield [dest] + suffix

                    # it is a constant to be substituted
                    else:

                        # base case when the constant is NOT replaced
                        yield [None] + suffix

                        for r, gs_dict in load_ct_gadgets_cache.items():
                            if current_sym in gs_dict.keys():

                                for r_ in Platform.ARM64.SUPPORTED_REGS:
                                    if (path_from[r_][r] is True) and (r_ not in suffix):

                                        yield [(r_, r)] + suffix

        # generator that returns all nodes that can reach dest_reg
        # NOTE: the dest_reg is a fixed destination, and the results contain all possible sources
        def _replace_dest_reg(dest_reg: str):

            for src, is_path in path_from[dest_reg].items():
                if is_path is True:
                    yield src

        # creates a new copy of the given Effect_ARM64,
        # and replaces all reg_in elements and constants, and, separately, the destination register
        # NOTE: it assumes there is no stack element
        def _apply_substitutions(ef: Effect_ARM64, syms_to_replace: List[str | int], replacements: List[str | None | Tuple[str, str]], dest_reg_replacement: str):

            def _rec_subst(el: Structured_element_ARM64):

                if el is None:
                    return None

                if el.type == "reg_in":
                    el.info["reg_name"] = replacements[syms_to_replace.index(el.info["reg_name"])]

                elif el.type == "ct_val":

                    subst = replacements[syms_to_replace.index(el.info["value"])]
                    if subst is not None:

                        el.type = "reg_in"
                        el.info = {"reg_name": subst[0]}

                elif el.is_op():
                    _rec_subst(el.info["term_1"])
                    _rec_subst(el.info["term_2"])

                elif el.type == "64b_stack_val":
                    raise RuntimeError("wanted Effect_ARM64 contains stack elements")

            subst_ef = deepcopy(ef)
            subst_ef.destination_element.info["reg_name"] = dest_reg_replacement

            if subst_ef.type == "ARITH":
                _rec_subst(subst_ef.params[0])

            return subst_ef

        # replace every Rk[i] with the corresponding Rx[i]
        # the rx should be part of the result yielded by replace seq gen called on rk
        # mss - max stack size
        def _genpath(rk: List[str | int], rx: List[str | None | Tuple[str, str]], mss: int):

            def _perm(n, k):

                if k == 0:
                    yield []

                else:
                    for suf in _perm(n, k - 1):
                        for i in range(n):
                            if i not in suf:
                                yield [i] + suf

            def _internal_genpath(_rk, _rx, _mss):

                if _mss < 1:
                    return

                if len(_rk) == 0:
                    yield [], 0

                else:

                    rk_current = _rk[0]
                    rx_current = _rx[0]

                    # Rx <- Rk register substitution
                    if rk_current in Platform.ARM64.SUPPORTED_REGS:

                        for trans_gs, stack_size in self.transition_chain_generator(rx_current, rk_current, trans_graph, path_from, _mss):
                            for suffix, suf_stack_size in _internal_genpath(_rk[1:], _rx[1:], _mss - stack_size):

                                yield trans_gs + suffix, suf_stack_size + stack_size

                    # Rx <- CT constant to register substitution
                    elif rx_current is not None:

                        subst_reg, loadct_reg = rx_current

                        for trans_gs, stack_size in self.transition_chain_generator(subst_reg, loadct_reg, trans_graph, path_from, _mss):
                            for loadct_g in load_ct_gadgets_cache[loadct_reg][rk_current]:

                                loadct_g_ss = loadct_g.get_stack_size()

                                for suffix, suf_stack_size in _internal_genpath(_rk[1:], _rx[1:], _mss - stack_size - loadct_g_ss):
                                    yield trans_gs + [loadct_g] + suffix, suf_stack_size + stack_size + loadct_g_ss

                    # CT remains unchanged
                    else:
                        yield from _internal_genpath(_rk[1:], _rx[1:], _mss)

            for p in _perm(len(rk), len(rk)):

                shuffled_rk = [rk[i] for i in p]
                shuffled_rx = [rx[i] for i in p]

                for path, stack_size in _internal_genpath(shuffled_rk, shuffled_rx, mss):
                    yield path, stack_size

        try:

            _init_ct_g_cache()

            wef_used_syms, dest_reg = _find_subst_symbols(wanted_effect)

            for repl_seq in _replace_seq_gen(wef_used_syms):
                for repl_dest_reg in _replace_dest_reg(dest_reg):

                    wef_subst = _apply_substitutions(wanted_effect, syms_to_replace=wef_used_syms,
                                                        replacements=repl_seq, dest_reg_replacement=repl_dest_reg)

                    gs = self.search_gadget(wef_subst, max_stack_size=max_stack_size, fixed_reg_list=[],
                                            reg_start_values=[], max_search_cnt=2 ** 63)

                    if len(gs) == 0:
                        continue

                    try:

                        # for each gadget that satisfies wanted Effect_ARM64 with substitutions
                        #   for each way to move the result from intermediary dest reg to the real dest reg
                        #       for each way to replace all the reg_in elements
                        for wef_g in gs:

                            mss_aux1 = max_stack_size - wef_g.get_stack_size()
                            for repl_dest_path, ss_1 in self.transition_chain_generator(dest_reg, repl_dest_reg, trans_graph, path_from, mss_aux1):

                                mss_aux2 = mss_aux1 - ss_1
                                if mss_aux2 < 0:
                                    raise RuntimeError(f"stack size bigger than mss in genpath: {mss_aux1}, max {ss_1}")

                                for path, ss_2 in _genpath(wef_used_syms, repl_seq, mss_aux2):

                                    if ss_2 > mss_aux2:
                                        raise RuntimeError(f"stack size bigger than mss in genpath: {ss_2}, max {mss_aux2}")

                                    # join from the end to the beginning: repl_dest_path, g, path
                                    final_path: List[ROP_gadget_ARM64]
                                    final_path = repl_dest_path + [wef_g] + path

                                    if ROP_searcher_ARM64._check_if_duplicate(path, b_cache) is True:
                                        continue

                                    candidate_ch = ROP_chain_ARM64.convert(final_path[-1].duplicate()[0])
                                    for g_aux in final_path[-2::-1]:

                                        candidate_ch_aux = candidate_ch.join(g_aux)
                                        candidate_ch.remove_stack_ids()
                                        candidate_ch = candidate_ch_aux

                                        if candidate_ch is None:
                                            break

                                    if candidate_ch is None:
                                        b_cache.update({_get_bytes(repl_dest_path + [wef_g] + path): False})
                                        continue

                                    if candidate_ch.get_stack_size() != ss_1 + ss_2 + wef_g.get_stack_size():
                                        raise RuntimeError(f"stack size inconsistent: expected {ss_1 + ss_2 + wef_g.get_stack_size()}, got {candidate_ch.get_stack_size()}")

                                    b = candidate_ch.get_bytes()
                                    if b in b_cache.keys():
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    b_cache.update({b: False})

                                    # check if the obtained result really matches the wanted Effect_ARM64
                                    # (check is needed because the transition paths' side effects are not taken into account when searching)

                                    # also, the start values are plugged in
                                    candidate_ch_cpy, org_to_fstid = candidate_ch.duplicate()

                                    start_values_cpy = deepcopy(reg_start_values)
                                    candidate_ch_cpy.effects = Effect_ARM64.join_effects(start_values_cpy, candidate_ch_cpy.effects)

                                    to_check_effect: Effect_ARM64 = None
                                    for ef in candidate_ch_cpy.effects:

                                        if ef.destination_element.info["reg_name"] == dest_reg:
                                            to_check_effect = ef
                                            break

                                    if to_check_effect is None:
                                        candidate_ch_cpy.remove_stack_ids()
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    wanted_effect_cpy = deepcopy(wanted_effect)

                                    if wanted_effect_cpy.match(to_check_effect) is False:
                                        candidate_ch_cpy.remove_stack_ids()
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    if candidate_ch_cpy.check_fixed_regs(fixed_reg_list) is False:
                                        candidate_ch_cpy.remove_stack_ids()
                                        candidate_ch.remove_stack_ids()
                                        continue

                                    # match arith might have changed some stack values
                                    accepted_chain, org_to_sndid = candidate_ch.duplicate(copy_stack_associated_values=True)

                                    for org_stack_elem in candidate_ch.stack.elements:
                                        if org_stack_elem.type == "64b_stack_val":

                                            fstid = _get_new_id(org_to_fstid, org_stack_elem.info["id"])

                                            if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[fstid]:
                                                raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[fstid]}")

                                            if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[fstid]:

                                                if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                                    raise RuntimeError("original chain and cloned chain non-null values are different")

                                                sndid = _get_new_id(org_to_sndid, org_stack_elem.info["id"])
                                                Stack_view.stack_values[sndid] = Stack_view.stack_values[fstid]

                                    candidate_ch_cpy.remove_stack_ids()
                                    candidate_ch.remove_stack_ids()

                                    b_cache[b] = True
                                    yield accepted_chain

                    finally:

                        g: ROP_gadget_ARM64
                        for g in gs:
                            g.remove_stack_ids()

        finally:
            pass

            # trans graph items are no longer local

            # clean up unused stack ids
            '''for _, d in trans_graph.items():
                for _, gs in d.items():
                    if gs is not None:
                        for g in gs:
                            g.remove_stack_ids()'''

    def _search_by_bruteforce(self, wanted_effect: Effect_ARM64, max_stack_size: int = 20, fixed_reg_list: List[str] = [],
                                reg_start_values: List[Effect_ARM64] = [], b_cache: Dict[bytes, bool] = {}):

        print("search by bruteforce")

        max_g_cnt = self.BRUTEFORCE_DEPTH
        if max_g_cnt > 4:
            raise RuntimeError(f"Cannot search chains by bruteforce for depth {max_g_cnt} - max allowed is 6")

        if max_g_cnt < 1:
            return

        def _get_bytes(chs: List[ROP_chain_ARM64]):

            b_repr = b''
            for ch in chs:
                b_repr += ch.get_bytes()

            return b_repr

        def _get_new_id(old_new_id: Dict[int, int], old_id: int):

            if old_id in old_new_id.keys():
                return old_new_id[old_id]

            return None

        # function that creates a rop chain
        # from a copy of a given gadget input,
        # and taking into account reg start values
        def _prepare(g: ROP_gadget_ARM64) -> ROP_chain_ARM64:

            cpy_g, _ = g.duplicate()
            return ROP_chain_ARM64.convert(cpy_g)

        # all the gadgets converted to atomic chains
        base_gs: List[ROP_chain_ARM64] = [_prepare(g) for g in self.gadgets]

        # yields all different arrangements of gadgets of length k
        def _get_paths(k: int):

            if k == 0:
                yield [], 0

            else:
                for suf, stack_size in _get_paths(k - 1):
                    for g in base_gs:
                        yield [g] + suf, stack_size + g.get_stack_size()

        try:

            for l in range(max_g_cnt):
                for path, stack_size in _get_paths(l + 1):

                    if stack_size > max_stack_size:
                        continue

                    if ROP_searcher_ARM64._check_if_duplicate(path, b_cache) is True:
                        continue

                    candidate_ch: ROP_chain_ARM64 = path[-1].duplicate()[0]
                    for g in path[-2::-1]:

                        candidate_ch_aux = candidate_ch.join(g)
                        candidate_ch.remove_stack_ids()
                        candidate_ch = candidate_ch_aux

                        if candidate_ch is None:
                            break

                    if candidate_ch is None:
                        b_cache.update({_get_bytes(path): False})
                        continue

                    b = candidate_ch.get_bytes()
                    if b in b_cache.keys():
                        candidate_ch.remove_stack_ids()
                        continue

                    b_cache.update({b: False})

                    wef_dest_reg_found = False

                    g: ROP_gadget_ARM64
                    for g in path:
                        for ef in g.effects:
                            if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == wanted_effect.destination_element.info["reg_name"]:
                                wef_dest_reg_found = True

                    if wef_dest_reg_found is False:
                        continue

                    candidate_ch_aux, org_to_fstid = candidate_ch.duplicate()
                    wanted_effect_cpy = deepcopy(wanted_effect)

                    start_values_cpy = deepcopy(reg_start_values)
                    candidate_ch_aux.effects = Effect_ARM64.join_effects(start_values_cpy, candidate_ch_aux.effects)

                    to_check_effect: Effect_ARM64 = None
                    for ef in candidate_ch_aux.effects:
                        if ef.type != "JUMP" and ef.destination_element.info["reg_name"] == wanted_effect_cpy.destination_element.info["reg_name"]:
                            to_check_effect = ef
                            break

                    if to_check_effect is None:
                        candidate_ch_aux.remove_stack_ids()
                        candidate_ch.remove_stack_ids()
                        continue

                    if wanted_effect_cpy.match(to_check_effect) is False:
                        candidate_ch_aux.remove_stack_ids()
                        candidate_ch.remove_stack_ids()
                        continue

                    if candidate_ch_aux.check_fixed_regs(fixed_reg_list) is False:
                        candidate_ch_aux.remove_stack_ids()
                        candidate_ch.remove_stack_ids()
                        continue

                    for org_stack_elem in candidate_ch.stack.elements:
                        if org_stack_elem.type == "64b_stack_val":

                            fstid = _get_new_id(org_to_fstid, org_stack_elem.info["id"])

                            if Stack_view.related_jump[org_stack_elem.info["id"]] != Stack_view.related_jump[fstid]:
                                raise RuntimeError(f"unexpected different associated jump IDs: {Stack_view.related_jump[org_stack_elem.info['id']]} {Stack_view.related_jump[fstid]}")

                            if Stack_view.stack_values[org_stack_elem.info["id"]] != Stack_view.stack_values[fstid]:

                                if Stack_view.stack_values[org_stack_elem.info["id"]] != None:
                                    raise RuntimeError("original chain and cloned chain non-null values are different")

                                Stack_view.stack_values[org_stack_elem.info["id"]] = Stack_view.stack_values[fstid]

                    candidate_ch_aux.remove_stack_ids()

                    b_cache[b] = True
                    yield candidate_ch

        finally:

            for ch in base_gs:
                ch.remove_stack_ids()

# FIXME temporary version, not sure it is useful to keep it in this format
#       (at least for ARM64)
class ROP_payload_ARM64:

    class _addr:

        def __init__(self, addr: int):
            self.addr = addr

    def __init__(self, rop_searcher, output_handle = stdout):

        self.logger = Logger(session_name = "PAYLOAD BUILDER", output_handle = output_handle)
        self.rop_searcher: ROP_searcher_ARM64 = rop_searcher

        self.pad_byte = b'A'
        self.max_b_size = 160
        self.forbidden_bytes: List[bytes] = []

        self._raw_payload: List[ROP_payload_ARM64._addr | bytes | ROP_chain_ARM64] = []

    def set_max_size(self, max_byte_size: int) -> None:
        self.max_b_size = max_byte_size

    def add_chain(self, ch: ROP_chain_ARM64) -> None:
        self._raw_payload.append(ch)

    def add_bytes(self, b: bytes) -> None:
        self._raw_payload.append(b)

    def add_padding(self, pad_len: int)-> None:
        self._raw_payload.append(self.pad_byte * pad_len)

    # add address for the last jump of a previous chain
    # its use is much more restricted than for x86 64
    def add_addr(self, addr: int) -> None:
        self._raw_payload.append(ROP_payload_ARM64._addr(addr))

    # remove the last added element
    def remove_last_added(self) -> None:

        if len(self._raw_payload) > 0:
            self._raw_payload.pop()

    # method responsible for building final payload, in bytes
    # NOTE: addr offset does NOT apply to given "naked" addresses,
    #       only to chain(gadget) addresses (and ret-only alignment gadgets)
    def build(self, chain_addr_offset: int = 0) -> bytes:

        _t = self.logger.log_info("Building payload...", start_timer = True)

        # check if bytes contain any forbidden byte
        def _check_bytes(to_check: bytes):

            for b in to_check:
                if b in self.forbidden_bytes:
                    return False

            return True

        replace_with_bytes = []
        # [(start index in final payload, custom bytes)]

        end_addrs = []
        # [end address for chain 0, ... end address for last chain]

        # preprocess the payload:
        #   * join adjacent chains
        #   * absorb padding, custom bytes, and addresses
        def _preprocess():

            _pad_total_offset = 0

            preproc_payload = []

            acc_chain: ROP_chain_ARM64 = None
            for item in self._raw_payload:

                if type(item) == ROP_payload_ARM64._addr:

                    if acc_chain is None:
                        self.logger.log_warning("cannot associate address with a chain when building payload")
                        return None

                    end_addrs.append(item)
                    preproc_payload.append(acc_chain)

                    _pad_total_offset += acc_chain.end_sp_pos
                    acc_chain = None

                elif type(item) == ROP_chain_ARM64:

                    if acc_chain is None:
                        acc_chain = item.duplicate()[0]

                    else:
                        acc_chain_aux = acc_chain.join(item)
                        acc_chain.remove_stack_ids()
                        acc_chain = acc_chain_aux

                        if acc_chain is None:
                            return None

                elif type(item) == bytes:

                    pad_offset = acc_chain.end_sp_pos + _pad_total_offset
                    pad_len = len(item)

                    pad_chain = ROP_chain_ARM64()
                    pad_chain.effects.append(Effect_ARM64.make_arith_ct_effect("add", "sp", pad_len))
                    pad_chain.valid_jump = True
                    pad_chain.valid_stack_access = True
                    pad_chain.end_sp_pos = len(item)

                    if acc_chain is None:
                        acc_chain = pad_chain

                    else:
                        acc_chain_aux = acc_chain.join(pad_chain)
                        acc_chain.remove_stack_ids()
                        acc_chain = acc_chain_aux

                        if acc_chain is None:
                            return None

                    replace_with_bytes.append((pad_offset, item))

            return preproc_payload

        preproc_payload = _preprocess()

        if preproc_payload is None:
            self.logger.log_warning(f"Could not join given chains (most likely, incompatible stacks)", end_timer = _t)
            return None, None

        b_mss = self.max_b_size

        payload = b''
        chain_start_addr = 0

        for idx in range(len(preproc_payload)):

            item = preproc_payload[idx]

            if type(item) is not ROP_chain_ARM64:
                raise RuntimeError(f"unexpected item {item} when building payload")

            if b_mss < 8:
                self.logger.log_warning(f"Not enough payload length for constructing payload for a given chain: only {b_mss} bytes left", end_timer = _t)

            if len(end_addrs) == idx:
                self.logger.log_warning("Payload builder could not find an end address for the chain; returning chain with last jump unassigned...", end_timer = _t)
                return payload

            last_jump_addr = to_bytes(end_addrs[idx].addr)

            if _check_bytes(last_jump_addr) is False:
                self.logger.log_warning("Provided jump address contains forbidden bytes", end_timer = _t)
                return None, None

            payload_, start_addr = item._make_payload(max_stack_size = b_mss, forbidden_bytes = self.forbidden_bytes,
                                                        addr_offset = chain_addr_offset, pad_byte = self.pad_byte, last_jump = end_addrs[idx].addr)
            if payload_ is not None:

                payload += payload_
                b_mss -= len(payload_)

                if b_mss < 0:
                    self.logger.log_warning(f"Maximum payload byte size surpassed by (at least) {-b_mss} bytes", end_timer = _t)
                    return None, None

                if idx == 0:
                    chain_start_addr = start_addr

            else:
                self.logger.log_warning("Creating payload for a given rop chain failed", end_timer = _t)
                return None, None

        # TODO check for incompatibility on the stack ?????
        # current implementation does not know whether it stepped over useful payload or not

        # replace pad with bytes
        for off_, content in replace_with_bytes:
            payload = payload[:off_] + content + payload[off_ + len(content):]

        self.logger.log_success("Successfully built the payload", end_timer = _t)

        return payload, chain_start_addr