diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index accad7f2..9adbddea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,28 @@ on: workflow_dispatch: jobs: - ci: + ecosystem: uses: angr/ci-settings/.github/workflows/angr-ci.yml@master - windows: - uses: ./.github/workflows/windows.yml - macos: - uses: ./.github/workflows/macos.yml + + test: + name: Test + strategy: + matrix: + os: [windows-2022, macos-15-intel, macos-15, ubuntu-24.04] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + submodules: 'recursive' + - name: Activate msvc + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 + if: startsWith(runner.os, 'windows') + - name: Setup uv + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6 + - name: Sync dependencies + run: uv sync -p 3.10 + - name: Run tests + run: uv run pytest tests diff --git a/.github/workflows/custom.yml b/.github/workflows/custom.yml deleted file mode 100644 index b8423966..00000000 --- a/.github/workflows/custom.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Custom CI - -on: - workflow_dispatch: - inputs: - image: - description: Container image to run with - type: string - required: true - nightly: - description: Run in nightly mode (include slow tests, no dependent projects) - type: boolean - required: true - afl: - description: Set parameters for AFL - type: boolean - required: true - - -jobs: - ci: - uses: angr/ci-settings/.github/workflows/angr-ci.yml@master - with: - container_image: ${{ inputs.image }} - nightly: ${{ inputs.nightly }} - afl: ${{ inputs.afl }} - - diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index e3a7a4b3..00000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Test on macOS - -on: - workflow_dispatch: - workflow_call: - -jobs: - macos: - name: Test macOS - runs-on: macos-12 - steps: - - uses: actions/checkout@v3 - with: - path: pyvex - submodules: true - - uses: actions/checkout@v3 - with: - repository: angr/binaries - path: binaries - - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - run: python -m venv $HOME/venv - name: Create venv - shell: bash - - run: | - source $HOME/venv/bin/activate - pip install git+https://github.com/angr/archinfo.git - name: Install dependencies - - run: | - source $HOME/venv/bin/activate - pip install ./pyvex[testing] - name: Install - - run: | - source $HOME/venv/bin/activate - pytest -n auto pyvex - name: Run pytest diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index e9bdc630..00000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Test on Windows - -on: - workflow_dispatch: - workflow_call: - -jobs: - windows: - name: Test Windows - runs-on: windows-2022 - steps: - - uses: actions/checkout@v3 - with: - path: pyvex - submodules: true - - uses: actions/checkout@v3 - with: - repository: angr/binaries - path: binaries - - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - run: python -m venv $HOME/venv - name: Create venv - shell: bash - - run: | - call %USERPROFILE%\venv\Scripts\activate - pip install git+https://github.com/angr/archinfo.git - name: Install dependencies - shell: cmd - - run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" - call %USERPROFILE%\venv\Scripts\activate - pip install ./pyvex[testing] - name: Install - shell: cmd - - run: | - call %USERPROFILE%\venv\Scripts\activate - pytest -n auto pyvex - name: Run pytest - shell: cmd diff --git a/.gitignore b/.gitignore index 585f4d51..89ac25c2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ pyvex/include vex-master vex-master.tar.gz docs/_build +scikit_build +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a36a32e..af3a7c32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,13 +5,13 @@ repos: # - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.24.1 hooks: - id: validate-pyproject fail_fast: true - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: # General - id: check-merge-conflict @@ -33,7 +33,7 @@ repos: fail_fast: true - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: check-ast fail_fast: true @@ -42,14 +42,8 @@ repos: # Modifiers # -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 - hooks: - - id: setup-cfg-fmt - args: ["--include-version-classifiers"] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: mixed-line-ending - id: trailing-whitespace @@ -60,20 +54,20 @@ repos: - id: rm-unneeded-f-str - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.2 + rev: v0.14.14 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] # Last modifier: Coding Standard -- repo: https://github.com/psf/black - rev: 24.4.2 +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 26.1.0 hooks: - id: black @@ -93,7 +87,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: debug-statements - id: check-builtin-literals diff --git a/.readthedocs.yml b/.readthedocs.yml index a2836ba6..8cb0229e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,6 +3,9 @@ version: 2 +sphinx: + configuration: docs/conf.py + submodules: include: all diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..b231b5c4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,164 @@ +cmake_minimum_required(VERSION 3.15) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +project(pyvex LANGUAGES C) + +# Set the output directory for built libraries +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/pyvex/lib) + +# Set the C standard to C99 +set(CMAKE_C_STANDARD 99) + +# Include directories +include_directories( + ${CMAKE_SOURCE_DIR}/pyvex/include + ${CMAKE_SOURCE_DIR}/pyvex_c + ${CMAKE_SOURCE_DIR}/vex/pub +) + +# Source files for the pyvex C library +set(PYVEX_SRC + pyvex_c/pyvex.c + pyvex_c/analysis.c + pyvex_c/logging.c + pyvex_c/postprocess.c +) + +# Source files for the VEX C library +set(VEX_SRC + vex/priv/ir_defs.c + vex/priv/ir_match.c + vex/priv/ir_opt.c + vex/priv/ir_inject.c + vex/priv/main_globals.c + vex/priv/main_util.c + vex/priv/s390_disasm.c + vex/priv/host_x86_defs.c + vex/priv/host_amd64_defs.c + vex/priv/host_arm_defs.c + vex/priv/host_arm64_defs.c + vex/priv/host_ppc_defs.c + vex/priv/host_riscv64_defs.c + vex/priv/host_s390_defs.c + vex/priv/host_mips_defs.c + vex/priv/host_x86_isel.c + vex/priv/host_amd64_isel.c + vex/priv/host_arm_isel.c + vex/priv/host_arm64_isel.c + vex/priv/host_ppc_isel.c + vex/priv/host_riscv64_isel.c + vex/priv/host_s390_isel.c + vex/priv/host_mips_isel.c + vex/priv/host_generic_maddf.c + vex/priv/host_generic_regs.c + vex/priv/host_generic_simd64.c + vex/priv/host_generic_simd128.c + vex/priv/host_generic_simd256.c + vex/priv/host_generic_reg_alloc2.c + vex/priv/host_generic_reg_alloc3.c + vex/priv/guest_generic_x87.c + vex/priv/guest_generic_bb_to_IR.c + vex/priv/guest_x86_helpers.c + vex/priv/guest_amd64_helpers.c + vex/priv/guest_arm_helpers.c + vex/priv/guest_arm64_helpers.c + vex/priv/guest_ppc_helpers.c + vex/priv/guest_riscv64_helpers.c + vex/priv/guest_s390_helpers.c + vex/priv/guest_mips_helpers.c + vex/priv/guest_x86_toIR.c + vex/priv/guest_amd64_toIR.c + vex/priv/guest_arm_toIR.c + vex/priv/guest_arm64_toIR.c + vex/priv/guest_ppc_toIR.c + vex/priv/guest_riscv64_toIR.c + vex/priv/guest_s390_toIR.c + vex/priv/guest_mips_toIR.c + vex/priv/multiarch_main_main.c +) + +# Build the VEX static library +add_library(vex STATIC ${VEX_SRC}) +target_compile_definitions(vex PRIVATE PYVEX) +target_include_directories(vex PUBLIC ${CMAKE_SOURCE_DIR}/vex/pub) + +# Build the shared library +add_library(pyvex SHARED ${PYVEX_SRC}) +set_target_properties(pyvex PROPERTIES OUTPUT_NAME "pyvex") + +# Handle .def file for Windows builds +if (WIN32) + set_target_properties(pyvex PROPERTIES LINK_FLAGS "/DEF:${CMAKE_SOURCE_DIR}/pyvex_c/pyvex.def") +endif() + +target_include_directories(pyvex PRIVATE pyvex_c) + +target_link_libraries(pyvex PRIVATE vex) + +# Install the built library to the Python package +# It is installed twice to handle both editable and non-editable installs +install(TARGETS pyvex DESTINATION ${CMAKE_SOURCE_DIR}/pyvex/lib) +install(TARGETS pyvex DESTINATION pyvex/lib) + +# --- BEGIN: Generate pub/libvex_guest_offsets.h --- +add_executable(genoffsets vex/auxprogs/genoffsets.c) +set_target_properties(genoffsets PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/vex/auxprogs) + +add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/vex/pub/libvex_guest_offsets.h + COMMAND $ > ${CMAKE_SOURCE_DIR}/vex/pub/libvex_guest_offsets.h + DEPENDS genoffsets + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Generating pub/libvex_guest_offsets.h" +) + +add_custom_target(generate_offsets_header + DEPENDS ${CMAKE_SOURCE_DIR}/vex/pub/libvex_guest_offsets.h +) +install( + FILES ${CMAKE_SOURCE_DIR}/vex/pub/libvex_guest_offsets.h + DESTINATION pyvex/include +) + +add_dependencies(vex generate_offsets_header) +# --- END: Generate pub/libvex_guest_offsets.h --- + +# --- BEGIN: Generate pyvex/vex_ffi.py --- +add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/pyvex/vex_ffi.py + COMMAND ${CMAKE_COMMAND} -E env + ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/make_ffi.py ${CMAKE_SOURCE_DIR}/vex/pub + DEPENDS ${CMAKE_SOURCE_DIR}/vex/pub/libvex_guest_offsets.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Generating pyvex/vex_ffi.py using make_ffi.py" +) + +add_custom_target(generate_vex_ffi_py + DEPENDS ${CMAKE_SOURCE_DIR}/pyvex/vex_ffi.py +) +install( + FILES ${CMAKE_SOURCE_DIR}/pyvex/vex_ffi.py + DESTINATION pyvex +) +add_dependencies(pyvex generate_vex_ffi_py) +# --- END: Generate pyvex/vex_ffi.py --- + +# --- BEGIN: Copy headers to pyvex/include --- +add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/pyvex/include/pub + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/vex/pub ${CMAKE_SOURCE_DIR}/pyvex/include/ + DEPENDS ${CMAKE_SOURCE_DIR}/vex/pub + COMMENT "Copying vex/pub to pyvex/include/" +) +add_custom_command( + OUTPUT ${CMAKE_SOURCE_DIR}/pyvex/include/pyvex.h + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/pyvex_c/pyvex.h ${CMAKE_SOURCE_DIR}/pyvex/include/pyvex.h + DEPENDS ${CMAKE_SOURCE_DIR}/pyvex_c/pyvex.h + COMMENT "Copying pyvex_c/pyvex.h to pyvex/include/" +) +add_custom_target(copy_headers ALL + DEPENDS ${CMAKE_SOURCE_DIR}/pyvex/include/pub ${CMAKE_SOURCE_DIR}/pyvex/include/pyvex.h +) +add_dependencies(pyvex copy_headers) +add_dependencies(copy_headers generate_offsets_header) +# --- END: Copy headers to pyvex/include --- diff --git a/README.md b/README.md index b5fcaf51..50993481 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ print(irsb.tyenv.types) print(irsb.tyenv.types[0]) ``` -Keep in mind that this is a *syntactic* respresentation of a basic block. That is, it'll tell you what the block means, but you don't have any context to say, for example, what *actual* data is written by a store instruction. +Keep in mind that this is a *syntactic* representation of a basic block. That is, it'll tell you what the block means, but you don't have any context to say, for example, what *actual* data is written by a store instruction. ## VEX Intermediate Representation @@ -82,7 +82,7 @@ To deal with widely diverse architectures, it is useful to carry out analyses on An IR abstracts away several architecture differences when dealing with different architectures, allowing a single analysis to be run on all of them: - **Register names.** The quantity and names of registers differ between architectures, but modern CPU designs hold to a common theme: each CPU contains several general purpose registers, a register to hold the stack pointer, a set of registers to store condition flags, and so forth. The IR provides a consistent, abstracted interface to registers on different platforms. Specifically, VEX models the registers as a separate memory space, with integer offsets (i.e., AMD64's `rax` is stored starting at address 16 in this memory space). -- **Memory access.** Different architectures access memory in different ways. For example, ARM can access memory in both little-endian and big-endian modes. The IR must abstracts away these differences. +- **Memory access.** Different architectures access memory in different ways. For example, ARM can access memory in both little-endian and big-endian modes. The IR must abstract away these differences. - **Memory segmentation.** Some architectures, such as x86, support memory segmentation through the use of special segment registers. The IR understands such memory access mechanisms. - **Instruction side-effects.** Most instructions have side-effects. For example, most operations in Thumb mode on ARM update the condition flags, and stack push/pop instructions update the stack pointer. Tracking these side-effects in an *ad hoc* manner in the analysis would be crazy, so the IR makes these effects explicit. diff --git a/fuzzing/build.sh b/fuzzing/build.sh index 9600e474..1d46da09 100755 --- a/fuzzing/build.sh +++ b/fuzzing/build.sh @@ -17,10 +17,10 @@ # Since pyvex requires a specific developer build of archinfo, install it from source cd "$SRC"/archinfo -python3.9 -m pip install . +python3 -m pip install . cd "$SRC"/pyvex -python3.9 -m pip install .[testing] +python3 -m pip install .[testing] # Generate a simple binary for the corpus echo -ne "start:\n\txor %edi, %edi\nmov \$60, %eax\nsyscall" > /tmp/corpus.s diff --git a/fuzzing/enhanced_fdp.py b/fuzzing/enhanced_fdp.py index 8fa0b397..c4721327 100644 --- a/fuzzing/enhanced_fdp.py +++ b/fuzzing/enhanced_fdp.py @@ -16,6 +16,7 @@ """ Defines the EnhancedFuzzedDataProvider """ + from atheris import FuzzedDataProvider diff --git a/pyproject.toml b/pyproject.toml index 23395128..4ec7f036 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,75 @@ [build-system] -requires = ["setuptools>=59", "wheel", "cffi >= 1.0.3"] -build-backend = "setuptools.build_meta" +requires = ["scikit-build-core >= 0.11.4", "cffi >= 1.0.3;implementation_name == 'cpython'"] +build-backend = "scikit_build_core.build" + +[project] +name = "pyvex" +description = "A Python interface to libVEX and VEX IR" +license = "BSD-2-Clause AND GPL-2.0-only" +license-files = [ + "LICENSE", + "pyvex_c/LICENSE", + "vex/LICENSE.README", + "vex/LICENSE.GPL", +] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +requires-python = ">=3.10" +dependencies = [ + "bitstring", + "cffi>=1.0.3;implementation_name == 'cpython'", +] +dynamic = ["version"] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.urls] +Homepage = "https://api.angr.io/projects/pyvex/en/latest/" +Repository = "https://github.com/angr/pyvex" + +[project.optional-dependencies] +docs = [ + "furo", + "myst-parser", + "sphinx", + "sphinx-autodoc-typehints", +] +fuzzing = [ + "atheris>=2.3.0", +] +testing = [ + "pytest", + "pytest-xdist", +] + +[dependency-groups] +dev = [ + "pytest>=8.4.1", +] + +[tool.scikit-build] +build-dir = "scikit_build" + +[tool.scikit-build.sdist] +include = [ + "pyvex/py.typed", + "pyvex/include/*", +] +exclude = [ + "tests*" +] + +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.regex" +input = "pyvex/__init__.py" [tool.black] line-length = 120 diff --git a/pyvex/__init__.py b/pyvex/__init__.py index cad9f930..b436b5f9 100644 --- a/pyvex/__init__.py +++ b/pyvex/__init__.py @@ -3,7 +3,7 @@ For an introduction to VEX, take a look here: https://docs.angr.io/advanced-topics/ir """ -__version__ = "9.2.112.dev0" +__version__ = "9.2.198.dev0" from . import const, expr, stmt from .arches import ( @@ -23,6 +23,7 @@ ARCH_RISCV64_LE, ARCH_S390X, ARCH_X86, + ARCH_TILEGX, ) from .block import IRSB, IRTypeEnv from .const import get_type_size, get_type_spec_size, tag_to_const_class @@ -89,4 +90,5 @@ "ARCH_MIPS64_BE", "ARCH_MIPS64_LE", "ARCH_RISCV64_LE", + "ARCH_TILEGX", ] diff --git a/pyvex/arches.py b/pyvex/arches.py index 79bf50e5..b67a7c1f 100644 --- a/pyvex/arches.py +++ b/pyvex/arches.py @@ -28,6 +28,7 @@ def __init__(self, name: str, bits: int, memory_endness: str, instruction_endnes "MIPS32": "VexArchMIPS32", "MIPS64": "VexArchMIPS64", "RISCV64": "VexArchRISCV64", + "TILEGX": "VexArchTILEGX", }[name] self.ip_offset = guest_offsets[ ( @@ -43,6 +44,7 @@ def __init__(self, name: str, bits: int, memory_endness: str, instruction_endnes "MIPS32": "pc", "MIPS64": "pc", "RISCV64": "pc", + "TILEGX": "pc", }[name], ) ] @@ -92,3 +94,4 @@ def get_register_offset(self, name: str) -> int: ARCH_MIPS64_BE = PyvexArch("MIPS64", 64, "Iend_BE") ARCH_MIPS64_LE = PyvexArch("MIPS64", 64, "Iend_LE") ARCH_RISCV64_LE = PyvexArch("RISCV64", 64, "Iend_LE", instruction_endness="Iend_LE") +ARCH_TILEGX = PyvexArch("TILEGX", 64, "Iend_LE") diff --git a/pyvex/block.py b/pyvex/block.py index ac996f33..ce598431 100644 --- a/pyvex/block.py +++ b/pyvex/block.py @@ -4,11 +4,12 @@ from typing import Optional from . import expr, stmt -from .const import get_type_size +from .const import U1, get_type_size +from .const_val import ConstVal from .data_ref import DataRef from .enums import VEXObject from .errors import SkipStatementsError -from .expr import RdTmp +from .expr import Const, RdTmp from .native import pvc from .stmt import ( CAS, @@ -49,13 +50,14 @@ class IRSB(VEXObject): :ivar int addr: The address of this basic block, i.e. the address in the first IMark """ - __slots__ = ( + __slots__ = [ "addr", "arch", "statements", "next", "_tyenv", "jumpkind", + "is_noop_block", "_direct_next", "_size", "_instructions", @@ -63,11 +65,13 @@ class IRSB(VEXObject): "default_exit_target", "_instruction_addresses", "data_refs", - ) + "const_vals", + ] # The following constants shall match the defs in pyvex.h MAX_EXITS = 400 MAX_DATA_REFS = 2000 + MAX_CONST_VALS = 1000 def __init__( self, @@ -126,15 +130,17 @@ def __init__( self.arch: Arch = arch self.statements: list[IRStmt] = [] - self.next: IRExpr | None = None + self.next: IRExpr = Const(U1(0)) self._tyenv: Optional["IRTypeEnv"] = None - self.jumpkind: str | None = None + self.jumpkind: str = "UNSET" self._direct_next: bool | None = None self._size: int | None = None self._instructions: int | None = None self._exit_statements: tuple[tuple[int, int, IRStmt], ...] | None = None + self.is_noop_block: bool = False self.default_exit_target = None self.data_refs = () + self.const_vals = () self._instruction_addresses: tuple[int, ...] = () if data is not None: @@ -551,6 +557,7 @@ def _from_c(self, lift_r, skip_stmts=False): self.next = expr.IRExpr._from_c(c_irsb.next) self.jumpkind = get_enum_from_int(c_irsb.jumpkind) self._size = lift_r.size + self.is_noop_block = lift_r.is_noop_block == 1 self._instructions = lift_r.insts self._instruction_addresses = tuple(itertools.islice(lift_r.inst_addrs, lift_r.insts)) @@ -578,9 +585,16 @@ def _from_c(self, lift_r, skip_stmts=False): self.data_refs = None if lift_r.data_ref_count > 0: if lift_r.data_ref_count > self.MAX_DATA_REFS: - raise SkipStatementsError("data_ref_count exceeded MAX_DATA_REFS (%d)" % self.MAX_DATA_REFS) + raise SkipStatementsError(f"data_ref_count exceeded MAX_DATA_REFS ({self.MAX_DATA_REFS})") self.data_refs = [DataRef.from_c(lift_r.data_refs[i]) for i in range(lift_r.data_ref_count)] + # Const values + self.const_vals = None + if lift_r.const_val_count > 0: + if lift_r.const_val_count > self.MAX_CONST_VALS: + raise SkipStatementsError(f"const_val_count exceeded MAX_CONST_VALS ({self.MAX_CONST_VALS})") + self.const_vals = [ConstVal.from_c(lift_r.const_vals[i]) for i in range(lift_r.const_val_count)] + def _set_attributes( self, statements=None, @@ -640,7 +654,7 @@ def __init__(self, arch, types=None): def __str__(self): return " ".join(("t%d:%s" % (i, t)) for i, t in enumerate(self.types)) - def lookup(self, tmp): + def lookup(self, tmp: int) -> str: """ Return the type of temporary variable `tmp` as an enum string """ diff --git a/pyvex/const.py b/pyvex/const.py index 7b00826a..123ad6bc 100644 --- a/pyvex/const.py +++ b/pyvex/const.py @@ -1,4 +1,6 @@ +# pylint:disable=missing-class-docstring,raise-missing-from,not-callable import re +from abc import ABC from .enums import VEXObject, get_enum_from_int from .errors import PyVEXError @@ -6,17 +8,17 @@ # IRConst hierarchy -class IRConst(VEXObject): +class IRConst(VEXObject, ABC): __slots__ = ["_value"] type: str - size: int | None = None + size: int tag: str c_constructor = None _value: int def pp(self): - print(self.__str__()) + print(str(self)) @property def value(self) -> int: @@ -215,6 +217,7 @@ class F32(IRConst): tag = "Ico_F32" op_format = "F32" c_constructor = pvc.IRConst_F32 + size = 32 def __init__(self, value): self._value = value @@ -234,6 +237,7 @@ class F32i(IRConst): tag = "Ico_F32i" op_format = "F32" c_constructor = pvc.IRConst_F32i + size = 32 def __init__(self, value): self._value = value @@ -253,6 +257,7 @@ class F64(IRConst): tag = "Ico_F64" op_format = "F64" c_constructor = pvc.IRConst_F64 + size = 64 def __init__(self, value): self._value = value @@ -272,6 +277,7 @@ class F64i(IRConst): tag = "Ico_F64i" op_format = "F64" c_constructor = pvc.IRConst_F64i + size = 64 def __init__(self, value): self._value = value @@ -291,6 +297,7 @@ class V128(IRConst): tag = "Ico_V128" op_format = "V128" c_constructor = pvc.IRConst_V128 + size = 128 def __init__(self, value): self._value = value @@ -317,6 +324,7 @@ class V256(IRConst): tag = "Ico_V256" op_format = "V256" c_constructor = pvc.IRConst_V256 + size = 256 def __init__(self, value): self._value = value diff --git a/pyvex/const_val.py b/pyvex/const_val.py new file mode 100644 index 00000000..cc751cc9 --- /dev/null +++ b/pyvex/const_val.py @@ -0,0 +1,26 @@ +class ConstVal: + """ + A constant value object. Indicates a constant value assignment to a VEX tmp variable. + + :ivar tmp: The tmp variable being assigned to. + :ivar value: The value of the tmp variable. + :ivar stmt_idx: The IRSB statement index containing the data access + """ + + __slots__ = ( + "tmp", + "value", + "stmt_idx", + ) + + def __init__(self, tmp: int, value: int, stmt_idx: int): + self.tmp = tmp + self.value = value + self.stmt_idx = stmt_idx + + def __repr__(self): + return f"" + + @classmethod + def from_c(cls, r): + return cls(r.tmp, r.value, r.stmt_idx) diff --git a/pyvex/expr.py b/pyvex/expr.py index 1a1b399f..8a069326 100644 --- a/pyvex/expr.py +++ b/pyvex/expr.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import logging import re -from typing import Optional +from typing import TYPE_CHECKING from .const import U8, U16, U32, U64, IRConst, get_type_size from .enums import IRCallee, IRRegArray, VEXObject, get_enum_from_int, get_int_from_enum from .errors import PyVEXError from .native import ffi, pvc +if TYPE_CHECKING: + from .block import IRTypeEnv + log = logging.getLogger("pyvex.expr") @@ -30,7 +35,7 @@ def _pp_str(self) -> str: raise NotImplementedError @property - def child_expressions(self) -> list["IRExpr"]: + def child_expressions(self) -> list[IRExpr]: """ A list of all of the expressions that this expression ends up evaluating. """ @@ -56,10 +61,10 @@ def constants(self): constants.append(v) return constants - def result_size(self, tyenv): + def result_size(self, tyenv: IRTypeEnv): return get_type_size(self.result_type(tyenv)) - def result_type(self, tyenv): + def result_type(self, tyenv: IRTypeEnv): raise NotImplementedError() def replace_expression(self, replacements): @@ -95,7 +100,7 @@ def replace_expression(self, replacements): v.replace_expression(replacements) @staticmethod - def _from_c(c_expr) -> Optional["IRExpr"]: + def _from_c(c_expr) -> IRExpr | None: if c_expr == ffi.NULL or c_expr[0] == ffi.NULL: return None @@ -282,7 +287,7 @@ class Get(IRExpr): tag = "Iex_Get" - def __init__(self, offset, ty: str, ty_int: int | None = None): + def __init__(self, offset: int, ty: str, ty_int: int | None = None): self.offset = offset if ty_int is None: self.ty_int = get_int_from_enum(ty) @@ -520,7 +525,7 @@ class Unop(IRExpr): tag = "Iex_Unop" - def __init__(self, op, args): + def __init__(self, op: str, args: list[IRExpr]): self.op = op self.args = args @@ -616,14 +621,14 @@ class Const(IRExpr): tag = "Iex_Const" - def __init__(self, con: "IRConst"): + def __init__(self, con: IRConst): self._con = con def _pp_str(self): return str(self.con) @property - def con(self) -> "IRConst": + def con(self) -> IRConst: return self._con @staticmethod @@ -849,6 +854,7 @@ def cmp_signature(op): if (m is None) == (m2 is None): raise PyvexOpMatchException() mfound = m if m is not None else m2 + assert mfound is not None size = int(mfound.group("size")) size_type = int_type_for_size(size) return (int_type_for_size(1), (size_type, size_type)) diff --git a/pyvex/lifting/libvex.py b/pyvex/lifting/libvex.py index 52329483..ae0e5c8d 100644 --- a/pyvex/lifting/libvex.py +++ b/pyvex/lifting/libvex.py @@ -26,6 +26,7 @@ "PPC64", "S390X", "RISCV64", + "TILEGX" } VEX_MAX_INSTRUCTIONS = 99 @@ -78,10 +79,6 @@ def _lift(self): if strict_block_end is None: strict_block_end = True - collect_data_refs = 1 if self.collect_data_refs else 0 - if collect_data_refs != 0 and self.load_from_ro_regions: - collect_data_refs |= 2 # the second bit stores load_from_ro_regions - if self.cross_insn_opt: px_control = VexRegisterUpdates.VexRegUpdUnwindregsAtMemAccess else: @@ -99,7 +96,9 @@ def _lift(self): self.traceflags, self.allow_arch_optimizations, strict_block_end, - collect_data_refs, + 1 if self.collect_data_refs else 0, + 1 if self.load_from_ro_regions else 0, + 1 if self.const_prop else 0, px_control, self.bytes_offset, ) diff --git a/pyvex/lifting/lift_function.py b/pyvex/lifting/lift_function.py index 446b2112..28b89cb3 100644 --- a/pyvex/lifting/lift_function.py +++ b/pyvex/lifting/lift_function.py @@ -34,6 +34,7 @@ def lift( collect_data_refs=False, cross_insn_opt=True, load_from_ro_regions=False, + const_prop=False, ): """ Recursively lifts blocks using the registered lifters and postprocessors. Tries each lifter in the order in @@ -78,14 +79,13 @@ def lift( if isinstance(data, (bytes, bytearray, memoryview)): py_data = data c_data = None - allow_arch_optimizations = False else: if max_bytes is None: raise PyVEXError("Cannot lift block with ffi pointer and no size (max_bytes is None)") c_data = data py_data = None - allow_arch_optimizations = True + allow_arch_optimizations = True # In order to attempt to preserve the property that # VEX lifts the same bytes to the same IR at all times when optimizations are disabled # we hack off all of VEX's non-IROpt optimizations when opt_level == -1. @@ -144,6 +144,7 @@ def lift( collect_data_refs=collect_data_refs, cross_insn_opt=cross_insn_opt, load_from_ro_regions=load_from_ro_regions, + const_prop=const_prop, ) except SkipStatementsError: assert skip_stmts is True @@ -160,6 +161,7 @@ def lift( collect_data_refs=collect_data_refs, cross_insn_opt=cross_insn_opt, load_from_ro_regions=load_from_ro_regions, + const_prop=const_prop, ) break except LiftingException as ex: @@ -210,6 +212,8 @@ def lift( strict_block_end=strict_block_end, skip_stmts=False, collect_data_refs=collect_data_refs, + load_from_ro_regions=load_from_ro_regions, + const_prop=const_prop, ) next_addr = addr + final_irsb.size @@ -235,6 +239,8 @@ def lift( inner=True, skip_stmts=False, collect_data_refs=collect_data_refs, + load_from_ro_regions=load_from_ro_regions, + const_prop=const_prop, ) if more_irsb.size: # Successfully decoded more bytes @@ -273,6 +279,8 @@ def lift( inner=inner, skip_stmts=False, collect_data_refs=collect_data_refs, + load_from_ro_regions=load_from_ro_regions, + const_prop=const_prop, ) except LiftingException: continue diff --git a/pyvex/lifting/lifter.py b/pyvex/lifting/lifter.py index 410a1948..c674f6d2 100644 --- a/pyvex/lifting/lifter.py +++ b/pyvex/lifting/lifter.py @@ -21,6 +21,7 @@ class Lifter: "addr", "cross_insn_opt", "load_from_ro_regions", + "const_prop", "disasm", "dump_irsb", ) @@ -63,6 +64,7 @@ def lift( collect_data_refs: bool = False, cross_insn_opt: bool = True, load_from_ro_regions: bool = False, + const_prop: bool = False, disasm: bool = False, dump_irsb: bool = False, ): @@ -102,6 +104,7 @@ def lift( self.irsb = irsb self.cross_insn_opt = cross_insn_opt self.load_from_ro_regions = load_from_ro_regions + self.const_prop = const_prop self.disasm = disasm self.dump_irsb = dump_irsb self._lift() diff --git a/pyvex/native.py b/pyvex/native.py index 34e47c08..03ef8567 100644 --- a/pyvex/native.py +++ b/pyvex/native.py @@ -1,4 +1,6 @@ +import getpass import hashlib +import importlib.resources import os import pickle import sys @@ -12,26 +14,9 @@ ffi = cffi.FFI() -def _locate_lib(module: str, library: str) -> str: - """ - Attempt to find a native library without using pkg_resources, and only fall back to pkg_resources upon failures. - This is because "import pkg_resources" is slow. - - :return: The full path of the native library. - """ - base_dir = os.path.dirname(__file__) - attempt = os.path.join(base_dir, library) - if os.path.isfile(attempt): - return attempt - - import pkg_resources # pylint:disable=import-outside-toplevel - - return pkg_resources.resource_filename(module, os.path.join("lib", library)) - - def _parse_ffi_str(): hash_ = hashlib.md5(_ffi_str.encode("utf-8")).hexdigest() - cache_location = os.path.join(tempfile.gettempdir(), f"pyvex_ffi_parser_cache.{hash_}") + cache_location = os.path.join(tempfile.gettempdir(), f"pyvex_ffi_parser_cache.{getpass.getuser()}.{hash_}") if os.path.isfile(cache_location): # load the cache @@ -46,8 +31,11 @@ def _parse_ffi_str(): "_declarations": ffi._parser._declarations, "_int_constants": ffi._parser._int_constants, } - with open(cache_location, "wb") as f: - f.write(pickle.dumps(cache)) + # atomically write cache + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(pickle.dumps(cache)) + temp_file_name = temp_file.name + os.replace(temp_file_name, cache_location) def _find_c_lib(): @@ -59,11 +47,12 @@ def _find_c_lib(): else: library_file = "libpyvex.so" - pyvex_path = _locate_lib(__name__, os.path.join("lib", library_file)) + pyvex_path = str(importlib.resources.files("pyvex") / "lib" / library_file) # parse _ffi_str and use cache if possible _parse_ffi_str() # RTLD_GLOBAL used for sim_unicorn.so lib = ffi.dlopen(pyvex_path) + if not lib.vex_init(): raise ImportError("libvex failed to initialize") # this looks up all the definitions (wtf) diff --git a/pyvex/stmt.py b/pyvex/stmt.py index 54528aa9..c1dd1c25 100644 --- a/pyvex/stmt.py +++ b/pyvex/stmt.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import logging from collections.abc import Iterator +from typing import TYPE_CHECKING from . import expr from .const import IRConst @@ -8,6 +11,9 @@ from .expr import Const, Get, IRExpr from .native import ffi, pvc +if TYPE_CHECKING: + from .block import IRTypeEnv + log = logging.getLogger("pyvex.stmt") @@ -16,7 +22,7 @@ class IRStmt(VEXObject): IR statements in VEX represents operations with side-effects. """ - tag: str | None = None + tag: str tag_int = 0 # set automatically at bottom of file __slots__ = [] @@ -25,7 +31,7 @@ def pp(self): print(str(self)) @property - def child_expressions(self) -> Iterator["IRExpr"]: + def child_expressions(self) -> Iterator[IRExpr]: for k in self.__slots__: v = getattr(self, k) if isinstance(v, IRExpr): @@ -54,7 +60,7 @@ def _from_c(c_stmt): raise PyVEXError("Unknown/unsupported IRStmtTag %s.\n" % get_enum_from_int(c_stmt.tag)) return stmt_class._from_c(c_stmt) - def typecheck(self, tyenv): # pylint: disable=unused-argument,no-self-use + def typecheck(self, tyenv: IRTypeEnv) -> bool: # pylint: disable=unused-argument,no-self-use return True def replace_expression(self, replacements): @@ -165,7 +171,7 @@ class Put(IRStmt): tag = "Ist_Put" - def __init__(self, data: "IRExpr", offset): + def __init__(self, data: IRExpr, offset: int): self.data = data self.offset = offset @@ -234,7 +240,7 @@ class WrTmp(IRStmt): tag = "Ist_WrTmp" - def __init__(self, tmp, data: "IRExpr"): + def __init__(self, tmp, data: IRExpr): self.tmp = tmp self.data = data @@ -272,7 +278,7 @@ class Store(IRStmt): tag = "Ist_Store" - def __init__(self, addr: "IRExpr", data: "IRExpr", end: str): + def __init__(self, addr: IRExpr, data: IRExpr, end: str): self.addr = addr self.data = data self.end = end @@ -403,7 +409,7 @@ class LLSC(IRStmt): tag = "Ist_LLSC" - def __init__(self, addr, storedata, result, end): + def __init__(self, addr: IRExpr, storedata: IRExpr, result: int, end: str): self.addr = addr self.storedata = storedata self.result = result @@ -527,7 +533,7 @@ class Exit(IRStmt): tag = "Ist_Exit" - def __init__(self, guard, dst, jk, offsIP): + def __init__(self, guard: IRExpr, dst: IRConst, jk: str, offsIP: int): self.guard = guard self.dst = dst self.offsIP = offsIP @@ -581,7 +587,7 @@ class LoadG(IRStmt): tag = "Ist_LoadG" - def __init__(self, end, cvt, dst, addr, alt, guard): + def __init__(self, end: str, cvt: str, dst: int, addr: IRExpr, alt: IRExpr, guard: IRExpr): self.addr = addr self.alt = alt self.guard = guard diff --git a/pyvex_c/Makefile b/pyvex_c/Makefile deleted file mode 100644 index 755edc74..00000000 --- a/pyvex_c/Makefile +++ /dev/null @@ -1,58 +0,0 @@ -UNAME := $(shell uname) -ifeq ($(UNAME), Darwin) - LIBRARY_FILE=libpyvex.dylib - STATIC_LIBRARY_FILE=libpyvex.a - LDFLAGS=-Wl,-install_name,@rpath/$(LIBRARY_FILE) -endif -ifeq ($(UNAME), Linux) - LIBRARY_FILE=libpyvex.so - STATIC_LIBRARY_FILE=libpyvex.a - LDFLAGS=-Wl,-soname,$(LIBRARY_FILE) -endif -ifeq ($(UNAME), FreeBSD) - LIBRARY_FILE=libpyvex.so - STATIC_LIBRARY_FILE=libpyvex.a - LDFLAGS=-Wl,-soname,$(LIBRARY_FILE) -endif -ifeq ($(UNAME), NetBSD) - LIBRARY_FILE=libpyvex.so - STATIC_LIBRARY_FILE=libpyvex.a - LDFLAGS=-Wl,-soname,$(LIBRARY_FILE) -endif -ifeq ($(UNAME), OpenBSD) - LIBRARY_FILE=libpyvex.so - LDFLAGS=-Wl,-soname,$(LIBRARY_FILE) -L/usr/local/lib -lvex -endif -ifeq ($(findstring MINGW,$(UNAME)), MINGW) - LIBRARY_FILE=pyvex.dll - STATIC_LIBRARY_FILE=libpyvex.a - LDFLAGS= -endif - -# deeply evil -# https://www.cmcrossroads.com/article/gnu-make-meets-file-names-spaces-them -sp =$(null) $(null) -qs = $(subst ?,$(sp),$1) -sq = $(subst $(sp),?,$1) - -CC?=gcc -AR=ar -INCFLAGS=-I "$(VEX_INCLUDE_PATH)" -CFLAGS=-g -O2 -Wall -shared -fPIC -std=c99 $(INCFLAGS) - -OBJECTS=pyvex.o logging.o analysis.o postprocess.o -HEADERS=pyvex.h - -all: $(LIBRARY_FILE) $(STATIC_LIBRARY_FILE) - -%.o: %.c - $(CC) -c $(CFLAGS) $< - -$(LIBRARY_FILE): $(OBJECTS) $(HEADERS) $(call sq,$(VEX_LIB_PATH)/libvex.a) - $(CC) $(CFLAGS) -o $(LIBRARY_FILE) $(OBJECTS) "$(VEX_LIB_PATH)/libvex.a" $(LDFLAGS) - -$(STATIC_LIBRARY_FILE): $(OBJECTS) $(HEADERS) $(call sq,$(VEX_LIB_PATH)/libvex.a) - $(AR) rcs $(STATIC_LIBRARY_FILE) $(OBJECTS) - -clean: - rm -f $(LIBRARY_FILE) $(STATIC_LIBRARY_FILE) *.o diff --git a/pyvex_c/Makefile-msvc b/pyvex_c/Makefile-msvc deleted file mode 100644 index b80c3cac..00000000 --- a/pyvex_c/Makefile-msvc +++ /dev/null @@ -1,10 +0,0 @@ -CC=cl -INCFLAGS=/I "$(VEX_INCLUDE_PATH)" -CFLAGS=/LD /O2 $(INCFLAGS) -LDFLAGS=/link /DEF:pyvex.def - -pyvex.dll: postprocess.c analysis.c pyvex.c logging.c "$(VEX_LIB_FILE)" pyvex.h pyvex.def - $(CC) $(CFLAGS) pyvex.c postprocess.c analysis.c logging.c "$(VEX_LIB_FILE)" $(LDFLAGS) - -clean: - del pyvex.dll pyvex.lib pyvex.exp pyvex.obj logging.obj diff --git a/pyvex_c/analysis.c b/pyvex_c/analysis.c index 459a0b38..559441b1 100644 --- a/pyvex_c/analysis.c +++ b/pyvex_c/analysis.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "pyvex.h" @@ -296,6 +297,21 @@ static void addToHHW(HashHW* h, HWord key, HWord val) h->used++; } +/* Remove key from the map. */ + +static void removeFromHHW(HashHW* h, HWord key) +{ + Int i, j; + + /* Find and replace existing binding, if any. */ + for (i = 0; i < h->used; i++) { + if (h->inuse[i] && h->key[i] == key) { + h->inuse[i] = False; + return; + } + } +} + /* Create keys, of the form ((minoffset << 16) | maxoffset). */ static UInt mk_key_GetPut ( Int offset, IRType ty ) @@ -322,18 +338,19 @@ void record_data_reference( lift_r->data_refs[idx].data_type = data_type; lift_r->data_refs[idx].stmt_idx = stmt_idx; lift_r->data_refs[idx].ins_addr = inst_addr; + lift_r->data_ref_count++; } - lift_r->data_ref_count++; } -Addr record_const( +Addr get_const_and_record( VEXLiftResult *lift_r, IRExpr *const_expr, Int size, DataRefTypes data_type, Int stmt_idx, Addr inst_addr, - Addr next_inst_addr) { + Addr next_inst_addr, + Bool record) { if (const_expr->tag != Iex_Const) { // Why are you calling me? @@ -343,12 +360,29 @@ Addr record_const( Addr addr = get_value_from_const_expr(const_expr->Iex.Const.con); if (addr != next_inst_addr) { - record_data_reference(lift_r, addr, size, data_type, stmt_idx, inst_addr); + if (record) { + record_data_reference(lift_r, addr, size, data_type, stmt_idx, inst_addr); + } return addr; } return -1; } +void record_tmp_value( + VEXLiftResult *lift_r, + Int tmp, + ULong value, + Int stmt_idx +) { + if (lift_r->const_val_count < MAX_CONST_VALS) { + Int idx = lift_r->const_val_count; + lift_r->const_vals[idx].tmp = tmp; + lift_r->const_vals[idx].value = value; + lift_r->const_vals[idx].stmt_idx = stmt_idx; + lift_r->const_val_count++; + } +} + typedef struct { int used; @@ -534,11 +568,14 @@ Bool reset_initial_register_values() } -void collect_data_references( +void execute_irsb( IRSB *irsb, VEXLiftResult *lift_r, VexArch guest, - Bool load_from_ro_regions) { + Bool load_from_ro_regions, + Bool collect_data_refs, + Bool const_prop +) { Int i; Addr inst_addr = -1, next_inst_addr = -1; @@ -601,16 +638,19 @@ void collect_data_references( if (data->Iex.Load.addr->tag == Iex_Const) { Int size; size = sizeofIRType(typeOfIRTemp(irsb->tyenv, stmt->Ist.WrTmp.tmp)); - Addr v = record_const(lift_r, data->Iex.Load.addr, size, Dt_Integer, i, inst_addr, next_inst_addr); + Addr v = get_const_and_record(lift_r, data->Iex.Load.addr, size, Dt_Integer, i, inst_addr, next_inst_addr, collect_data_refs); if (v != -1 && v != next_inst_addr) { last_const_value = v; } // Load the value if it might be a constant pointer... - if (load_from_ro_regions && guest == VexArchARM && size == 4) { - UInt value; + if (load_from_ro_regions) { + UInt value = 0; if (load_value(data->Iex.Load.addr->Iex.Const.con->Ico.U32, size, data->Iex.Load.end, &value)) { tmps[stmt->Ist.WrTmp.tmp].used = 1; tmps[stmt->Ist.WrTmp.tmp].value = value; + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, value, i); + } } } } else if (data->Iex.Load.addr->tag == Iex_RdTmp) { @@ -620,16 +660,21 @@ void collect_data_references( Int size; size = sizeofIRType(typeOfIRTemp(irsb->tyenv, stmt->Ist.WrTmp.tmp)); if (tmps[rdtmp].value != last_const_value) { - record_data_reference(lift_r, tmps[rdtmp].value, size, Dt_Integer, i, inst_addr); + if (collect_data_refs) { + record_data_reference(lift_r, tmps[rdtmp].value, size, Dt_Integer, i, inst_addr); + } } if (load_from_ro_regions) if (guest == VexArchARM && size == 4 || guest == VexArchMIPS32 && size == 4 || guest == VexArchMIPS64 && size == 8) { - ULong value; + ULong value = 0; if (load_value(tmps[rdtmp].value, size, data->Iex.Load.end, &value)) { tmps[stmt->Ist.WrTmp.tmp].used = 1; tmps[stmt->Ist.WrTmp.tmp].value = value; + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, value, i); + } } } } @@ -643,13 +688,18 @@ void collect_data_references( Addr addr = get_value_from_const_expr(arg1->Iex.Const.con) + get_value_from_const_expr(arg2->Iex.Const.con); if (data->Iex.Binop.op == Iop_Add32) { - addr &= 0xffffffff; - } + addr &= 0xffffffff; + } if (addr != next_inst_addr) { if (addr != last_const_value) { - record_data_reference(lift_r, addr, 0, Dt_Unknown, i, inst_addr); + if (collect_data_refs) { + record_data_reference(lift_r, addr, 0, Dt_Unknown, i, inst_addr); + } } } + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, addr, i); + } } else { // Do the calculation if (arg1->tag == Iex_RdTmp @@ -662,25 +712,15 @@ void collect_data_references( value &= 0xffffffff; } if (value != last_const_value) { - record_data_reference(lift_r, value, 0, Dt_Unknown, i, inst_addr); + if (collect_data_refs) { + record_data_reference(lift_r, value, 0, Dt_Unknown, i, inst_addr); + } } tmps[stmt->Ist.WrTmp.tmp].used = 1; tmps[stmt->Ist.WrTmp.tmp].value = value; - } - if (arg1->tag == Iex_Const - && arg2->tag == Iex_RdTmp - && tmps[arg2->Iex.RdTmp.tmp].used) { - ULong arg1_value = get_value_from_const_expr(arg1->Iex.Const.con); - ULong arg2_value = tmps[arg2->Iex.RdTmp.tmp].value; - ULong value = arg1_value + arg2_value; - if (data->Iex.Binop.op == Iop_Add32) { - value &= 0xffffffff; - } - if (value != last_const_value) { - record_data_reference(lift_r, value, 0, Dt_Unknown, i, inst_addr); + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, value, i); } - tmps[stmt->Ist.WrTmp.tmp].used = 1; - tmps[stmt->Ist.WrTmp.tmp].value = value; } if (arg1->tag == Iex_Const && arg2->tag == Iex_RdTmp @@ -692,15 +732,22 @@ void collect_data_references( value &= 0xffffffff; } if (value != last_const_value) { - record_data_reference(lift_r, value, 0, Dt_Unknown, i, inst_addr); + if (collect_data_refs) { + record_data_reference(lift_r, value, 0, Dt_Unknown, i, inst_addr); + } } tmps[stmt->Ist.WrTmp.tmp].used = 1; tmps[stmt->Ist.WrTmp.tmp].value = value; + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, value, i); + } } if (arg2->tag == Iex_Const) { ULong arg2_value = get_value_from_const_expr(arg2->Iex.Const.con); if (arg2_value != last_const_value) { - record_data_reference(lift_r, arg2_value, 0, Dt_Unknown, i, inst_addr); + if (collect_data_refs) { + record_data_reference(lift_r, arg2_value, 0, Dt_Unknown, i, inst_addr); + } } } if (arg1->tag == Iex_RdTmp @@ -715,19 +762,22 @@ void collect_data_references( } tmps[stmt->Ist.WrTmp.tmp].used = 1; tmps[stmt->Ist.WrTmp.tmp].value = value; + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, value, i); + } } } } else { // Normal binary operations if (data->Iex.Binop.arg1->tag == Iex_Const) { - Addr v = record_const(lift_r, data->Iex.Binop.arg1, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + Addr v = get_const_and_record(lift_r, data->Iex.Binop.arg1, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); if (v != -1 && v != next_inst_addr) { last_const_value = v; } } if (data->Iex.Binop.arg2->tag == Iex_Const) { - Addr v = record_const(lift_r, data->Iex.Binop.arg2, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + Addr v = get_const_and_record(lift_r, data->Iex.Binop.arg2, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); if (v != -1 && v != next_inst_addr) { last_const_value = v; } @@ -736,21 +786,25 @@ void collect_data_references( break; case Iex_Const: { - Addr v = record_const(lift_r, data, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + Addr v = get_const_and_record(lift_r, data, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); if (v != -1 && v != next_inst_addr) { last_const_value = v; } + Addr value = get_value_from_const_expr(data->Iex.Const.con); tmps[stmt->Ist.WrTmp.tmp].used = 1; - tmps[stmt->Ist.WrTmp.tmp].value = get_value_from_const_expr(data->Iex.Const.con); + tmps[stmt->Ist.WrTmp.tmp].value = value; + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, value, i); + } } break; case Iex_ITE: { if (data->Iex.ITE.iftrue->tag == Iex_Const) { - record_const(lift_r, data->Iex.ITE.iftrue, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + get_const_and_record(lift_r, data->Iex.ITE.iftrue, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); } if (data->Iex.ITE.iffalse->tag == Iex_Const) { - record_const(lift_r, data->Iex.ITE.iffalse, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + get_const_and_record(lift_r, data->Iex.ITE.iffalse, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); } } break; @@ -761,6 +815,9 @@ void collect_data_references( if (lookupHHW(env, &val, key) == True) { tmps[stmt->Ist.WrTmp.tmp].used = 1; tmps[stmt->Ist.WrTmp.tmp].value = val; + if (const_prop) { + record_tmp_value(lift_r, stmt->Ist.WrTmp.tmp, val, i); + } } } default: @@ -781,7 +838,7 @@ void collect_data_references( IRExpr *data = stmt->Ist.Put.data; if (data->tag == Iex_Const) { - Addr v = record_const(lift_r, data, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + Addr v = get_const_and_record(lift_r, data, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); if (v != -1 && v != next_inst_addr) { last_const_value = v; } @@ -795,9 +852,21 @@ void collect_data_references( ULong value = tmps[data->Iex.RdTmp.tmp].value; addToHHW(env, key, value); if (value != last_const_value) { - record_data_reference(lift_r, value, 0, Dt_Integer, i, inst_addr); + if (collect_data_refs) { + record_data_reference(lift_r, value, 0, Dt_Integer, i, inst_addr); + } } } + else { + // the tmp does not exist; we ignore updates to GP on MIPS32 + // this is to handle cases where gp is loaded from a stack variable + if (guest == VexArchMIPS32 && stmt->Ist.Put.offset == offsetof(VexGuestMIPS32State, guest_r28)) { + break; + } + IRType data_type = typeOfIRExpr(irsb->tyenv, data); + UInt key = mk_key_GetPut(stmt->Ist.Put.offset, data_type); + removeFromHHW(env, key); + } } } break; @@ -814,12 +883,12 @@ void collect_data_references( if (data_type != Ity_INVALID) { data_size = sizeofIRType(data_type); } - record_const(lift_r, store_dst, data_size, + get_const_and_record(lift_r, store_dst, data_size, data_size == 0? Dt_Unknown : Dt_StoreInteger, - i, inst_addr, next_inst_addr); + i, inst_addr, next_inst_addr, collect_data_refs); } if (store_data->tag == Iex_Const) { - record_const(lift_r, store_data, 0, Dt_Unknown, i, inst_addr, next_inst_addr); + get_const_and_record(lift_r, store_data, 0, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); } } break; @@ -829,7 +898,7 @@ void collect_data_references( if (stmt->Ist.Dirty.details->mAddr != NULL && stmt->Ist.Dirty.details->mAddr->tag == Iex_Const) { IRExpr *m_addr = stmt->Ist.Dirty.details->mAddr; - record_const(lift_r, m_addr, stmt->Ist.Dirty.details->mSize, Dt_FP, i, inst_addr, next_inst_addr); + get_const_and_record(lift_r, m_addr, stmt->Ist.Dirty.details->mSize, Dt_FP, i, inst_addr, next_inst_addr, collect_data_refs); } break; case Ist_LoadG: @@ -843,7 +912,7 @@ void collect_data_references( if (data_type != Ity_INVALID) { data_size = sizeofIRType(data_type); } - record_const(lift_r, addr, data_size, Dt_Unknown, i, inst_addr, next_inst_addr); + get_const_and_record(lift_r, addr, data_size, Dt_Unknown, i, inst_addr, next_inst_addr, collect_data_refs); } break; default: @@ -856,3 +925,61 @@ void collect_data_references( free(tmps); } } + +/* Determine if the VEX block is an no-op */ +void get_is_noop_block( + IRSB *irsb, VEXLiftResult *lift_r +) { + // the block is a noop block if it only has IMark statements **and** it jumps to its immediate successor. VEX will + // generate such blocks when opt_level==1 and cross_insn_opt is True. + + // the block is a noop block if it only has IMark statements and IP-setting statements that set the IP to the next + // location. VEX will generate such blocks when opt_level==1 and cross_insn_opt is False. + Addr fallthrough_addr = 0xffffffffffffffff; + Bool has_other_inst = False; + + for (int i = 0; i < irsb->stmts_used; ++i) { + IRStmt *stmt = irsb->stmts[i]; + if (stmt->tag == Ist_IMark) { + // update fallthrough_addr; it will be correct upon the last instruction + fallthrough_addr = stmt->Ist.IMark.addr + stmt->Ist.IMark.delta + stmt->Ist.IMark.len; + } else if (stmt->tag == Ist_NoOp) { + // NoOp is a no-op + } else if (stmt->tag == Ist_Put) { + if (stmt->Ist.Put.data->tag == Iex_Const) { + if (irsb->offsIP != stmt->Ist.Put.offset) { + // found a register write that is not the same as the pc offset; this is not a noop block + lift_r->is_noop_block = False; + return; + } + } else { + // found a non-constant register write; this is not a noop block + lift_r->is_noop_block = False; + return; + } + } else { + has_other_inst = True; + break; + } + } + if (has_other_inst) { + lift_r->is_noop_block = False; + return; + } + + if (fallthrough_addr == 0xffffffffffffffff) { + // for some reason we cannot find the fallthrough addr; just give up + lift_r->is_noop_block = False; + return; + } + + if (irsb->jumpkind == Ijk_Boring && irsb->next->tag == Iex_Const) { + if (irsb->next->Iex.Const.con->tag == Ico_U32 && fallthrough_addr < 0xffffffff && fallthrough_addr == irsb->next->Iex.Const.con->Ico.U32 + || irsb->next->Iex.Const.con->tag == Ico_U64 && fallthrough_addr == irsb->next->Iex.Const.con->Ico.U64) { + lift_r->is_noop_block = True; + return; + } + } + + lift_r->is_noop_block = False; +} diff --git a/pyvex_c/postprocess.c b/pyvex_c/postprocess.c index 7dc35d83..e2949574 100644 --- a/pyvex_c/postprocess.c +++ b/pyvex_c/postprocess.c @@ -237,7 +237,7 @@ void arm_post_processor_determine_calls( // Fix the not-default exit other_exit->Ist.Exit.jk = Ijk_Call; } - else { + else if (!has_exit || other_exit->Ist.Exit.jk != Ijk_Call) { //Fix the default exit irsb->jumpkind = Ijk_Call; } diff --git a/pyvex_c/pyvex.c b/pyvex_c/pyvex.c index caf6d554..6ab77d8c 100644 --- a/pyvex_c/pyvex.c +++ b/pyvex_c/pyvex.c @@ -279,6 +279,9 @@ static void vex_prepare_vai(VexArch arch, VexArchInfo *vai) { case VexArchRISCV64: vai->hwcaps = 0; break; + case VexArchTILEGX: + vai->hwcaps = 0; + break; default: pyvex_error("Invalid arch in vex_prepare_vai.\n"); break; @@ -306,7 +309,6 @@ static void vex_prepare_vbi(VexArch arch, VexAbiInfo *vbi) { } VEXLiftResult _lift_r; -#define LOAD_FROM_RO_REGIONS_MASK 2 //---------------------------------------------------------------------- // Main entry point. Do a lift. @@ -323,6 +325,8 @@ VEXLiftResult *vex_lift( int allow_arch_optimizations, int strict_block_end, int collect_data_refs, + int load_from_ro_regions, + int const_prop, VexRegisterUpdates px_control, unsigned int lookback) { VexRegisterUpdates pxControl = px_control; @@ -358,7 +362,9 @@ VEXLiftResult *vex_lift( // Do the actual translation if (setjmp(jumpout) == 0) { LibVEX_Update_Control(&vc); + _lift_r.is_noop_block = False; _lift_r.data_ref_count = 0; + _lift_r.const_val_count = 0; _lift_r.irsb = LibVEX_Lift(&vta, &vtr, &pxControl); if (!_lift_r.irsb) { // Lifting failed @@ -376,8 +382,9 @@ VEXLiftResult *vex_lift( arm_post_processor_determine_calls(_lift_r.inst_addrs[0], _lift_r.size, _lift_r.insts, _lift_r.irsb); } zero_division_side_exits(_lift_r.irsb); - if (collect_data_refs) { - collect_data_references(_lift_r.irsb, &_lift_r, guest, collect_data_refs & LOAD_FROM_RO_REGIONS_MASK); + get_is_noop_block(_lift_r.irsb, &_lift_r); + if (collect_data_refs || const_prop) { + execute_irsb(_lift_r.irsb, &_lift_r, guest, (Bool)load_from_ro_regions, (Bool)collect_data_refs, (Bool)const_prop); } return &_lift_r; } else { diff --git a/pyvex_c/pyvex.h b/pyvex_c/pyvex.h index abd687c4..32383bd9 100644 --- a/pyvex_c/pyvex.h +++ b/pyvex_c/pyvex.h @@ -40,12 +40,20 @@ typedef struct _DataRef { Addr ins_addr; } DataRef; +typedef struct _ConstVal { + Int tmp; + Int stmt_idx; + ULong value; // 64-bit max +} ConstVal; + #define MAX_EXITS 400 #define MAX_DATA_REFS 2000 +#define MAX_CONST_VALS 1000 typedef struct _VEXLiftResult { IRSB* irsb; Int size; + Bool is_noop_block; // Conditional exits Int exit_count; ExitInfo exits[MAX_EXITS]; @@ -58,6 +66,9 @@ typedef struct _VEXLiftResult { // Data references Int data_ref_count; DataRef data_refs[MAX_DATA_REFS]; + // Constant propagation + Int const_val_count; + ConstVal const_vals[MAX_CONST_VALS]; } VEXLiftResult; VEXLiftResult *vex_lift( @@ -72,6 +83,8 @@ VEXLiftResult *vex_lift( int allow_arch_optimizations, int strict_block_end, int collect_data_refs, + int load_from_ro_regions, + int const_prop, VexRegisterUpdates px_control, unsigned int lookback_amount); diff --git a/pyvex_c/pyvex_internal.h b/pyvex_c/pyvex_internal.h index 20aaf74c..aea2be5f 100644 --- a/pyvex_c/pyvex_internal.h +++ b/pyvex_c/pyvex_internal.h @@ -6,6 +6,7 @@ void mips32_post_processor_fix_unconditional_exit(IRSB *irsb); void remove_noops(IRSB* irsb); void zero_division_side_exits(IRSB* irsb); void get_exits_and_inst_addrs(IRSB *irsb, VEXLiftResult *lift_r); -void get_default_exit_target(IRSB *irsb, VEXLiftResult *lift_r ); -void collect_data_references(IRSB *irsb, VEXLiftResult *lift_r, VexArch guest, Bool load_from_ro_regions); +void get_default_exit_target(IRSB *irsb, VEXLiftResult *lift_r); +void get_is_noop_block(IRSB *irsb, VEXLiftResult *lift_r); +void execute_irsb(IRSB *irsb, VEXLiftResult *lift_r, VexArch guest, Bool load_from_ro_regions, Bool collect_data_refs, Bool const_prop); Addr get_value_from_const_expr(IRConst* con); diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 47c0fd9b..00000000 --- a/setup.cfg +++ /dev/null @@ -1,44 +0,0 @@ -[metadata] -name = pyvex -version = attr: pyvex.__version__ -description = A Python interface to libVEX and VEX IR -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/angr/pyvex -license = BSD-2-Clause -license_files = - LICENSE - pyvex_c/LICENSE -classifiers = - License :: OSI Approved :: BSD License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - -[options] -packages = find: -install_requires = - bitstring - cffi>=1.0.3;implementation_name == 'cpython' -python_requires = >=3.10 -include_package_data = True - -[options.extras_require] -docs = - furo - myst-parser - sphinx - sphinx-autodoc-typehints -fuzzing = - atheris>=2.3.0 -testing = - pytest - pytest-xdist - -[options.package_data] -pyvex = - lib/* - include/* - py.typed diff --git a/setup.py b/setup.py deleted file mode 100644 index eefd1c2a..00000000 --- a/setup.py +++ /dev/null @@ -1,160 +0,0 @@ -# pylint: disable=no-name-in-module,import-error,missing-class-docstring -import glob -import multiprocessing -import os -import platform -import shutil -import subprocess -import sys -from distutils.command.build import build as st_build -from distutils.util import get_platform - -from setuptools import setup -from setuptools.command.develop import develop as st_develop -from setuptools.command.sdist import sdist as st_sdist -from setuptools.errors import LibError - -PROJECT_DIR = os.path.dirname(os.path.realpath(__file__)) -LIB_DIR = os.path.join(PROJECT_DIR, "pyvex", "lib") -INCLUDE_DIR = os.path.join(PROJECT_DIR, "pyvex", "include") - - -if sys.platform in ("win32", "cygwin"): - LIBRARY_FILE = "pyvex.dll" - STATIC_LIBRARY_FILE = "pyvex.lib" -elif sys.platform == "darwin": - LIBRARY_FILE = "libpyvex.dylib" - STATIC_LIBRARY_FILE = "libpyvex.a" -else: - LIBRARY_FILE = "libpyvex.so" - STATIC_LIBRARY_FILE = "libpyvex.a" - - -VEX_LIB_NAME = "vex" # can also be vex-amd64-linux -VEX_PATH = os.path.abspath(os.path.join(PROJECT_DIR, "vex")) - - -def _build_vex(): - if len(os.listdir(VEX_PATH)) == 0: - raise LibError( - "vex submodule not cloned correctly, aborting.\nThis may be fixed with `git submodule update --init`" - ) - - e = os.environ.copy() - e["MULTIARCH"] = "1" - e["DEBUG"] = "1" - - if sys.platform == "win32": - cmd = ["nmake", "/f", "Makefile-msvc", "all"] - elif shutil.which("gmake") is not None: - cmd = ["gmake", "-f", "Makefile-gcc", "-j", str(multiprocessing.cpu_count()), "all"] - else: - cmd = ["make", "-f", "Makefile-gcc", "-j", str(multiprocessing.cpu_count()), "all"] - - try: - subprocess.run(cmd, cwd=VEX_PATH, env=e, check=True) - except FileNotFoundError as err: - raise LibError("Couldn't find " + cmd[0] + " in PATH") from err - except subprocess.CalledProcessError as err: - raise LibError("Error while building libvex: " + str(err)) from err - - -def _build_pyvex(): - e = os.environ.copy() - e["VEX_LIB_PATH"] = VEX_PATH - e["VEX_INCLUDE_PATH"] = os.path.join(VEX_PATH, "pub") - e["VEX_LIB_FILE"] = os.path.join(VEX_PATH, "libvex.lib") - - if sys.platform == "win32": - cmd = ["nmake", "/f", "Makefile-msvc"] - elif shutil.which("gmake") is not None: - cmd = ["gmake", "-f", "Makefile", "-j", str(multiprocessing.cpu_count())] - else: - cmd = ["make", "-f", "Makefile", "-j", str(multiprocessing.cpu_count())] - - try: - subprocess.run(cmd, cwd="pyvex_c", env=e, check=True) - except FileNotFoundError as err: - raise LibError("Couldn't find " + cmd[0] + " in PATH") from err - except subprocess.CalledProcessError as err: - raise LibError("Error while building libpyvex: " + str(err)) from err - - -def _shuffle_files(): - shutil.rmtree(LIB_DIR, ignore_errors=True) - shutil.rmtree(INCLUDE_DIR, ignore_errors=True) - os.mkdir(LIB_DIR) - os.mkdir(INCLUDE_DIR) - - pyvex_c_dir = os.path.join(PROJECT_DIR, "pyvex_c") - - shutil.copy(os.path.join(pyvex_c_dir, LIBRARY_FILE), LIB_DIR) - shutil.copy(os.path.join(pyvex_c_dir, STATIC_LIBRARY_FILE), LIB_DIR) - shutil.copy(os.path.join(pyvex_c_dir, "pyvex.h"), INCLUDE_DIR) - for f in glob.glob(os.path.join(VEX_PATH, "pub", "*")): - shutil.copy(f, INCLUDE_DIR) - - -def _clean_bins(): - shutil.rmtree(LIB_DIR, ignore_errors=True) - shutil.rmtree(INCLUDE_DIR, ignore_errors=True) - - -def _build_ffi(): - sys.path.append(".") # PEP 517 doesn't include . in sys.path - import make_ffi # pylint: disable=import-outside-toplevel - - sys.path.pop() - - make_ffi.doit(os.path.join(VEX_PATH, "pub")) - - -class build(st_build): - def run(self, *args): - self.execute(_build_vex, (), msg="Building libVEX") - self.execute(_build_pyvex, (), msg="Building libpyvex") - self.execute(_shuffle_files, (), msg="Copying libraries and headers") - self.execute(_build_ffi, (), msg="Creating CFFI defs file") - super().run(*args) - - -class develop(st_develop): - def run(self): - self.run_command("build") - super().run() - - -class sdist(st_sdist): - def run(self, *args): - self.execute(_clean_bins, (), msg="Removing binaries") - super().run(*args) - - -cmdclass = { - "build": build, - "develop": develop, - "sdist": sdist, -} - -try: - from setuptools.command.editable_wheel import editable_wheel as st_editable_wheel - - class editable_wheel(st_editable_wheel): - def run(self): - self.run_command("build") - super().run() - - cmdclass["editable_wheel"] = editable_wheel -except ModuleNotFoundError: - pass - -if "bdist_wheel" in sys.argv and "--plat-name" not in sys.argv: - sys.argv.append("--plat-name") - name = get_platform() - if "linux" in name: - sys.argv.append("manylinux2014_" + platform.machine()) - else: - # https://www.python.org/dev/peps/pep-0425/ - sys.argv.append(name.replace(".", "_").replace("-", "_")) - -setup(cmdclass=cmdclass) diff --git a/tests/test_arm_postprocess.py b/tests/test_arm_postprocess.py index 455ad554..b6784cf1 100644 --- a/tests/test_arm_postprocess.py +++ b/tests/test_arm_postprocess.py @@ -314,6 +314,21 @@ def test_arm_postprocess_call(): ) assert irsb.jumpkind == "Ijk_Call" + # 400000 str lr, [sp,#-0x4]! + # 400004 mov r1, #0xa + # 400008 cmp r0, r1 + # 40000c blne #FunctionB + irsb = pyvex.IRSB( + data=bytes.fromhex("04e02de50a10a0e3010050e10100001b"), + mem_addr=0x400000, + arch=pyvex.ARCH_ARM_LE, + num_inst=4, + opt_level=i, + ) + assert len(irsb.exit_statements) == 1 + assert irsb.exit_statements[0][2].jumpkind == "Ijk_Call" + assert irsb.jumpkind == "Ijk_Boring" + def test_arm_postprocess_ret(): for i in range(3): diff --git a/tests/test_lift.py b/tests/test_lift.py index 6a0416ab..a30ee1dc 100644 --- a/tests/test_lift.py +++ b/tests/test_lift.py @@ -26,7 +26,7 @@ class NOPLifter(GymratLifter): lifter = NOPLifter(pyvex.ARCH_AMD64, 0) # this should not throw an exception - block = lifter.lift("\x0F\x0Fa") + block = lifter.lift("\x0f\x0fa") assert block.size == 2 assert block.instructions == 1 assert block.jumpkind == JumpKind.NoDecode diff --git a/vex b/vex index a3f558b7..421bf0d9 160000 --- a/vex +++ b/vex @@ -1 +1 @@ -Subproject commit a3f558b7f6426d45688dbbf425bee1e4bea9cc5d +Subproject commit 421bf0d9ec800df09fe4f8d90a8c13a0c63325e3