├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── pip-audit.yml │ ├── release.yml │ ├── tests.yml │ └── zizmor.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── abi3audit ├── __init__.py ├── __main__.py ├── _audit.py ├── _cache.py ├── _cli.py ├── _extract.py ├── _object.py ├── _state.py └── _vendor │ ├── __init__.py │ ├── asn1_der.py │ └── mach_o.py ├── pyproject.toml └── test ├── __init__.py ├── test_audit.py ├── test_extract.py └── test_minimum_version.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: pip 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | with: 17 | persist-credentials: false 18 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 19 | with: 20 | python-version-file: pyproject.toml 21 | - run: | 22 | make lint INSTALL_EXTRA=lint 23 | 24 | check-readme: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | persist-credentials: false 30 | 31 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 32 | with: 33 | python-version-file: pyproject.toml 34 | 35 | - run: python -m pip install . 36 | 37 | - name: check-readme 38 | run: | 39 | diff \ 40 | <( \ 41 | awk '/@begin-abi3audit-help@/{f=1;next} /@end-abi3audit-help@/{f=0} f' \ 42 | < README.md | sed '1d;$d' \ 43 | ) \ 44 | <( \ 45 | python -m abi3audit --help \ 46 | ) 47 | -------------------------------------------------------------------------------- /.github/workflows/pip-audit.yml: -------------------------------------------------------------------------------- 1 | name: Scan dependencies for vulnerabilities with pip-audit 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "0 12 * * *" 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | pip-audit: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | 23 | - name: Install Python 24 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 25 | with: 26 | python-version-file: pyproject.toml 27 | 28 | - name: Run pip-audit 29 | uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0 30 | with: 31 | inputs: . 32 | local: true 33 | ignore-vulns: | 34 | GHSA-v3c5-jqr6-7qm8 35 | PYSEC-2022-42969 36 | GHSA-r9hx-vwmv-q579 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - published 5 | 6 | name: release 7 | 8 | permissions: {} # All jobs define their needed permissions below. 9 | 10 | jobs: 11 | build: 12 | name: Build distributions 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | persist-credentials: false 19 | 20 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 21 | with: 22 | python-version-file: pyproject.toml 23 | 24 | - name: Install pypa/build 25 | run: python -m pip install -U build 26 | 27 | - name: Build distributions 28 | run: python -m build 29 | 30 | - name: Store distributions 31 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 32 | with: 33 | name: python-package-distributions 34 | path: dist/ 35 | 36 | publish: 37 | name: upload distributions to PyPI 38 | needs: 39 | - build 40 | runs-on: ubuntu-latest 41 | permissions: 42 | id-token: write # Used to authenticate to PyPI via OIDC. 43 | 44 | steps: 45 | - name: Download distributions 46 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 47 | with: 48 | name: python-package-distributions 49 | path: dist/ 50 | 51 | - name: publish 52 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 53 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | python: 16 | - "3.9" 17 | - "3.10" 18 | - "3.11" 19 | - "3.12" 20 | - "3.13" 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | with: 25 | persist-credentials: false 26 | 27 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 28 | with: 29 | python-version: ${{ matrix.python }} 30 | 31 | - name: test 32 | run: make test INSTALL_EXTRA=test 33 | 34 | all-tests-pass: 35 | if: always() 36 | needs: 37 | - test 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: check test jobs 41 | uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 42 | with: 43 | jobs: ${{ toJSON(needs) }} 44 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 🌈 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | jobs: 10 | zizmor: 11 | name: zizmor latest via Cargo 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | # required for workflows in private repositories 16 | contents: read 17 | actions: read 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Install the latest version of uv 25 | uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 26 | 27 | - name: Run zizmor 🌈 28 | run: uvx zizmor --format sarif . > results.sarif 29 | env: 30 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Upload SARIF file 33 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 34 | with: 35 | sarif_file: results.sarif 36 | category: zizmor 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | env/ 3 | test/wheelhouse 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 William Woodruff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PY_MODULE := abi3audit 2 | 3 | # Optionally overriden by the user, if they're using a virtual environment manager. 4 | VENV ?= env 5 | VENV_EXISTS := $(VENV)/pyvenv.cfg 6 | 7 | # On Windows, venv scripts/shims are under `Scripts` instead of `bin`. 8 | VENV_BIN := $(VENV)/bin 9 | ifeq ($(OS),Windows_NT) 10 | VENV_BIN := $(VENV)/Scripts 11 | endif 12 | 13 | ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py' -not -path '$(PY_MODULE)/_vendor/*') \ 14 | $(shell find test -name '*.py') 15 | 16 | # Optionally overridden by the user in the `test` target. 17 | TESTS := 18 | 19 | # Optionally overridden by the user/CI, to limit the installation to a specific 20 | # subset of development dependencies. 21 | INSTALL_EXTRA := dev 22 | 23 | # If the user selects a specific test pattern to run, set `pytest` to fail fast 24 | # and only run tests that match the pattern. 25 | # Otherwise, run all tests and enable coverage assertions, since we expect 26 | # complete test coverage. 27 | ifneq ($(TESTS),) 28 | TEST_ARGS := -x -k $(TESTS) 29 | COV_ARGS := 30 | else 31 | TEST_ARGS := 32 | COV_ARGS := 33 | # TODO: Enable. 34 | # COV_ARGS := --fail-under 100 35 | endif 36 | 37 | .PHONY: all 38 | all: 39 | @echo "Run my targets individually!" 40 | 41 | .PHONY: dev 42 | dev: $(VENV_EXISTS) 43 | 44 | $(VENV_EXISTS): pyproject.toml 45 | # Create our Python 3 virtual environment 46 | python -m venv $(VENV) 47 | $(VENV_BIN)/python -m pip install --upgrade pip 48 | $(VENV_BIN)/python -m pip install -e .[$(INSTALL_EXTRA)] 49 | 50 | .PHONY: lint 51 | lint: $(VENV_EXISTS) 52 | . $(VENV_BIN)/activate && \ 53 | ruff format --check $(ALL_PY_SRCS) && \ 54 | ruff check $(ALL_PY_SRCS) && \ 55 | mypy $(PY_MODULE) 56 | # TODO: Enable. 57 | # interrogate -c pyproject.toml . 58 | 59 | .PHONY: format 60 | format: $(VENV_EXISTS) 61 | . $(VENV_BIN)/activate && \ 62 | ruff check --fix $(ALL_PY_SRCS) && \ 63 | ruff format $(ALL_PY_SRCS) 64 | 65 | .PHONY: test tests 66 | test tests: $(VENV_EXISTS) 67 | . $(VENV_BIN)/activate && \ 68 | pytest --cov=$(PY_MODULE) $(T) $(TEST_ARGS) && \ 69 | python -m coverage report -m $(COV_ARGS) 70 | 71 | .PHONY: doc 72 | doc: $(VENV_EXISTS) 73 | . $(VENV_BIN)/activate && \ 74 | command -v pdoc3 && \ 75 | PYTHONWARNINGS='error::UserWarning' pdoc --force --html $(PY_MODULE) 76 | 77 | .PHONY: dist 78 | dist: $(VENV_EXISTS) 79 | . $(VENV_BIN)/activate && \ 80 | python -m build 81 | 82 | .PHONY: edit 83 | edit: 84 | $(EDITOR) $(ALL_PY_SRCS) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abi3audit 2 | 3 | 4 | [![Tests](https://github.com/pypa/abi3audit/actions/workflows/tests.yml/badge.svg)](https://github.com/pypa/abi3audit/actions/workflows/tests.yml) 5 | [![PyPI version](https://badge.fury.io/py/abi3audit.svg)](https://pypi.org/project/abi3audit) 6 | [![Packaging status](https://repology.org/badge/tiny-repos/python:abi3audit.svg)](https://repology.org/project/python:abi3audit/versions) 7 | 8 | 9 | *[Read the Trail of Bits blog post about how we find bugs with `abi3audit`!](https://blog.trailofbits.com/2022/11/15/python-wheels-abi-abi3audit/)* 10 | 11 | `abi3audit` scans Python extensions for `abi3` violations and inconsistencies. 12 | 13 | It can scan individual (unpackaged) shared objects, packaged wheels, or entire 14 | package version histories. 15 | 16 | ![An animated demonstration of abi3audit in action](https://user-images.githubusercontent.com/3059210/194171233-a61a81d2-f2ed-4078-8988-903f996ba2e3.gif) 17 | 18 | This project is maintained in part by [Trail of Bits](https://trailofbits.com). 19 | This is not an official Trail of Bits product. 20 | 21 | ## Index 22 | 23 | * [Motivation](#motivation) 24 | * [Installation](#installation) 25 | * [Usage](#usage) 26 | * [Examples](#examples) 27 | * [Limitations](#limitations) 28 | * [Licensing](#licensing) 29 | 30 | ## Motivation 31 | 32 | CPython (the reference implementation of Python) defines a stable API and corresponding 33 | ABI ("`abi3`"). In principle, any CPython extension can be built against this 34 | API/ABI and will remain forward compatible with future minor versions of CPython. 35 | In other words: if you build against the stable ABI for Python 3.5, your 36 | extension should work without modification on Python 3.9. 37 | 38 | The stable ABI simplifies packaging of CPython extensions, since the packager 39 | only needs to build one `abi3` wheel that targets the minimum supported Python 40 | version. 41 | 42 | To signal that a Python wheel contains `abi3`-compatible extensions, 43 | the Python packaging ecosystem uses the `abi3` wheel tag, e.g.: 44 | 45 | ```text 46 | pyrage-1.0.1-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl 47 | ``` 48 | 49 | Unfortunately, there is **no actual enforcement** of `abi3` compliance 50 | in Python extensions at install or runtime: a wheel (or independent 51 | shared object) that is tagged as `abi3` is assumed to be `abi3`, but 52 | is not validated in any way. 53 | 54 | To make matters worse, there is **no formal connection** between the flag 55 | ([`--py-limited-api`](https://setuptools.pypa.io/en/latest/userguide/ext_modules.html#setuptools.Extension)) 56 | that controls wheel tagging and the build macros 57 | ([`Py_LIMITED_API`](https://docs.python.org/3/c-api/stable.html#c.Py_LIMITED_API)) 58 | that actually lock a Python extension into a specific `abi3` version. 59 | 60 | As a result: it is very easy to compile a Python extension for the wrong `abi3` 61 | version, or to tag a Python wheel as `abi3` without actually compiling it 62 | as `abi3`-compatible. 63 | 64 | This has serious security and reliability implications: non-stable parts 65 | of the CPython ABI can change between minor versions, resulting in crashes, 66 | unpredictable behavior, or potentially exploitable memory corruption when 67 | a Python extension incorrectly assumes the parameters of a function 68 | or layout of a structure. 69 | 70 | ## Installation 71 | 72 | `abi3audit` is available via `pip`: 73 | 74 | ```bash 75 | pip install abi3audit 76 | ``` 77 | 78 | ## Usage 79 | 80 | You can run `abi3audit` as a standalone program, or via `python -m abi3audit`: 81 | 82 | ```bash 83 | abi3audit --help 84 | python -m abi3audit --help 85 | ``` 86 | 87 | Top-level: 88 | 89 | 90 | ```console 91 | usage: abi3audit [-h] [-V] [--debug] [-v] [-R] [-o OUTPUT] [-s] [-S] 92 | [--assume-minimum-abi3 ASSUME_MINIMUM_ABI3] 93 | SPEC [SPEC ...] 94 | 95 | Scans Python extensions for abi3 violations and inconsistencies 96 | 97 | positional arguments: 98 | SPEC the files or other dependency specs to scan 99 | 100 | options: 101 | -h, --help show this help message and exit 102 | -V, --version show program's version number and exit 103 | --debug emit debug statements; this setting also overrides 104 | `ABI3AUDIT_LOGLEVEL` and is equivalent to setting it 105 | to `debug` 106 | -v, --verbose give more output, including pretty-printed results for 107 | each audit step 108 | -R, --report generate a JSON report; uses --output 109 | -o, --output OUTPUT the path to write the JSON report to (default: stdout) 110 | -s, --summary always output a summary even if there are no 111 | violations/ABI version mismatches 112 | -S, --strict fail the entire audit if an individual audit step 113 | fails 114 | --assume-minimum-abi3 ASSUME_MINIMUM_ABI3 115 | assumed abi3 version (3.x, with x>=2) if it cannot be 116 | detected 117 | ``` 118 | 119 | 120 | ### Examples 121 | 122 | Audit a single shared object, wheel, or PyPI package: 123 | 124 | ```bash 125 | # audit a local copy of an abi3 extension 126 | abi3audit procmaps.abi3.so 127 | 128 | # audit a local copy of an abi3 wheel 129 | abi3audit procmaps-0.5.0-cp36-abi3-manylinux2010_x86_64.whl 130 | 131 | # audit every abi3 wheel for the package 'procmaps' on PyPI 132 | abi3audit procmaps 133 | ``` 134 | 135 | Show additional detail (pretty tables and individual violations) while auditing: 136 | 137 | ```bash 138 | abi3audit procmaps --verbose 139 | ``` 140 | 141 | yields: 142 | 143 | ```console 144 | [17:59:46] 👎 procmaps: 145 | procmaps-0.5.0-cp36-abi3-manylinux2010_x86_64.whl: procmaps.abi3.so 146 | uses the Python 3.10 ABI, but is tagged for the Python 3.6 ABI 147 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓ 148 | ┃ Symbol ┃ Version ┃ 149 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩ 150 | │ PyUnicode_AsUTF8AndSize │ 3.10 │ 151 | └─────────────────────────┴─────────┘ 152 | [17:59:47] 💁 procmaps: 2 extensions scanned; 1 ABI version mismatches and 0 153 | ABI violations found 154 | ``` 155 | 156 | Generate a JSON report for each input: 157 | 158 | ```bash 159 | abi3audit procmaps --report | python -m json.tool 160 | ``` 161 | 162 | yields: 163 | 164 | ```json 165 | { 166 | "specs": { 167 | "procmaps": { 168 | "kind": "package", 169 | "package": { 170 | "procmaps-0.5.0-cp36-abi3-manylinux2010_x86_64.whl": [ 171 | { 172 | "name": "procmaps.abi3.so", 173 | "result": { 174 | "is_abi3": true, 175 | "is_abi3_baseline_compatible": false, 176 | "baseline": "3.6", 177 | "computed": "3.10", 178 | "non_abi3_symbols": [], 179 | "future_abi3_objects": { 180 | "PyUnicode_AsUTF8AndSize": "3.10" 181 | } 182 | } 183 | } 184 | ], 185 | "procmaps-0.6.1-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl": [ 186 | { 187 | "name": "procmaps.abi3.so", 188 | "result": { 189 | "is_abi3": true, 190 | "is_abi3_baseline_compatible": true, 191 | "baseline": "3.7", 192 | "computed": "3.7", 193 | "non_abi3_symbols": [], 194 | "future_abi3_objects": {} 195 | } 196 | } 197 | ] 198 | } 199 | } 200 | } 201 | } 202 | ``` 203 | 204 | ## Limitations 205 | 206 | `abi3audit` is a *best-effort* tool, with some of the same limitations as 207 | [`auditwheel`](https://github.com/pypa/auditwheel). In particular: 208 | 209 | * `abi3audit` cannot check for *dynamic* abi3 violations, such as an extension 210 | that calls [`dlsym(3)`](https://man7.org/linux/man-pages/man3/dlsym.3.html) 211 | to invoke a non-abi3 function at runtime. 212 | 213 | * `abi3audit` can confirm the presence of abi3-compatible symbols, but does 214 | not have an exhaustive list of abi3-*incompatible* symbols. Instead, it looks 215 | for violations by looking for symbols that start with `Py_` or `_Py_` that 216 | are not in the abi3 compatibility list. This is *unlikely* to result in false 217 | positives, but *could* if an extension incorrectly uses those reserved 218 | prefixes. 219 | 220 | * When auditing a "bare" shared object (e.g. `foo.abi3.so`), `abi3audit` cannot 221 | assume anything about the minimum *intended* abi3 version. Instead, it 222 | defaults to the lowest known abi3 version (`abi3-cp32`) and warns on any 223 | version mismatches (e.g., a symbol that was only stabilized in 3.6). 224 | This can result in false positives, so users are encouraged to audit entire 225 | wheels or packages instead (since they contain the sufficient metadata). 226 | 227 | * `abi3audit` considers the abi3 version when a symbol was *stabilized*, 228 | not *introduced*. In other words: `abi3audit` will produce a warning 229 | when an `abi3-cp36` extension contains a function stabilized in 3.7, even 230 | if that function was introduced in 3.6. This is *not* a false positive 231 | (it is an ABI version mismatch), but it's *generally* not a source of bugs. 232 | 233 | * `abi3audit` checks both the "local" and "external" symbols for each extension, 234 | for formats that support both. It does this to catch symbols that have been 235 | inlined, such as `_Py_DECREF`. However, if the extension's symbol table 236 | has been stripped, these may be missed. 237 | 238 | ## Licensing 239 | 240 | `abi3audit` is licensed under the MIT license. 241 | 242 | `abi3audit` includes ASN.1 and Mach-O parsers generated from 243 | definitions provided by the [Kaitai Struct](https://kaitai.io/) project. 244 | These vendored parsers are licensed by the Kaitai Struct authors under the MIT 245 | license. 246 | -------------------------------------------------------------------------------- /abi3audit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | abi3audit APIs. 3 | """ 4 | 5 | __version__ = "0.0.21" 6 | -------------------------------------------------------------------------------- /abi3audit/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The `python -m abi3audit` entrypoint. 3 | """ 4 | 5 | if __name__ == "__main__": 6 | from abi3audit._cli import main 7 | 8 | main() 9 | -------------------------------------------------------------------------------- /abi3audit/_audit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core auditing logic for shared objects. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import logging 8 | from dataclasses import dataclass 9 | from typing import Any 10 | 11 | from abi3info import DATAS, FUNCTIONS 12 | from abi3info.models import Data, Function, PyVersion, Symbol 13 | from rich.console import Console, ConsoleOptions, RenderResult 14 | from rich.table import Table 15 | 16 | from abi3audit._object import SharedObject 17 | from abi3audit._state import status 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | # A handpicked exclusion list of symbols that are not strictly in the limited API 22 | # or stable ABI, but in practice always appear in ABI3-compatible code. 23 | # Since they are not listed in CPython's `stable_abi.toml`, we maintain them here separately. 24 | # For more information, see https://github.com/pypa/abi3audit/issues/85 25 | # and https://github.com/wjakob/nanobind/discussions/500 . 26 | _ALLOWED_SYMBOLS: set[str] = {"Py_XDECREF"} 27 | 28 | 29 | class AuditError(Exception): 30 | pass 31 | 32 | 33 | @dataclass(frozen=True, eq=True) 34 | class AuditResult: 35 | so: SharedObject 36 | baseline: PyVersion 37 | computed: PyVersion 38 | non_abi3_symbols: set[Symbol] 39 | future_abi3_objects: set[Function | Data] 40 | 41 | def is_abi3(self) -> bool: 42 | return len(self.non_abi3_symbols) == 0 43 | 44 | def is_abi3_baseline_compatible(self) -> bool: 45 | # TODO(ww): Why does PyVersion.__le__ not typecheck as bool? 46 | return bool(self.baseline >= self.computed) 47 | 48 | def json(self) -> dict[str, Any]: 49 | return { 50 | "is_abi3": self.is_abi3(), 51 | "is_abi3_baseline_compatible": self.is_abi3_baseline_compatible(), 52 | "baseline": str(self.baseline), 53 | "computed": str(self.computed), 54 | "non_abi3_symbols": [sym.name for sym in self.non_abi3_symbols], 55 | "future_abi3_objects": { 56 | obj.symbol.name: str(obj.added) for obj in self.future_abi3_objects 57 | }, 58 | } 59 | 60 | def __bool__(self) -> bool: 61 | return self.is_abi3() and self.is_abi3_baseline_compatible() 62 | 63 | def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: 64 | """ 65 | The rich representation of an audit result. 66 | 67 | If problems are found, renders a table of ABI3 violations/mismatches with 68 | the name and ABI3 version (or a "not ABI3" marker) of each offending symbol. 69 | 70 | If no problems are found, renders a short "OK" message. 71 | """ 72 | has_violations = self.non_abi3_symbols or self.computed > self.baseline 73 | if has_violations: 74 | table = Table() 75 | table.add_column("Symbol") 76 | table.add_column("Version") 77 | 78 | if self.non_abi3_symbols: 79 | yield f"[red]:thumbs_down: [green]{self.so}[/green] has non-ABI3 symbols" 80 | if self.computed > self.baseline: 81 | yield ( 82 | f"[yellow]:thumbs_down: [green]{self.so}[/green] uses the Python " 83 | f"[blue]{self.computed}[/blue] ABI, but is tagged for the Python " 84 | f"[red]{self.baseline}[/red] ABI" 85 | ) 86 | for sym in self.non_abi3_symbols: 87 | table.add_row(sym.name, "not ABI3") 88 | for obj in self.future_abi3_objects: 89 | table.add_row(obj.symbol.name, str(obj.added)) 90 | yield table 91 | else: 92 | yield f"[green]:thumbs_up: {self.so}" 93 | 94 | 95 | def audit(so: SharedObject, assume_minimum_abi3: PyVersion = PyVersion(3, 2)) -> AuditResult: 96 | # We might fail to retrieve a minimum abi3 baseline if our context 97 | # (the shared object or its containing wheel) isn't actually tagged 98 | # as abi3 compatible. 99 | baseline = so.abi3_version(assume_lowest=assume_minimum_abi3) 100 | if baseline is None: 101 | raise AuditError("failed to determine ABI version baseline: not abi3 tagged?") 102 | 103 | # In principle, our computed abi3 version could be lower than our baseline version, 104 | # if for example an abi3-py36 wheel only used interfaces present in abi3-py35. 105 | # But this wouldn't actually *make* the wheel abi3-py35, since CPython 106 | # does not guarantee backwards compatibility between abi3 versions. 107 | computed = baseline 108 | non_abi3_symbols = set() 109 | future_abi3_objects = set() 110 | 111 | status.update(f"{so}: analyzing symbols") 112 | logger.debug(f"auditing {so}") 113 | try: 114 | for sym in so: 115 | maybe_abi3 = FUNCTIONS.get(sym) 116 | if maybe_abi3 is None: 117 | maybe_abi3 = DATAS.get(sym) 118 | 119 | if maybe_abi3 is not None: 120 | if maybe_abi3.added > computed: 121 | computed = maybe_abi3.added 122 | if maybe_abi3.added > baseline: 123 | future_abi3_objects.add(maybe_abi3) 124 | elif sym.name.startswith(("Py", "_Py")): 125 | # Exclude module initialization symbols. Technically 126 | # this should always be `PyInit_{filename}` but there can 127 | # be multiple if a shared object contains multiple extensions; 128 | # see https://github.com/pypa/abi3audit/issues/111. 129 | if sym.name.startswith("PyInit_"): 130 | continue 131 | # Local symbols are fine, since they are inlined functions 132 | # from the CPython limited API. 133 | if sym not in _ALLOWED_SYMBOLS and sym.visibility != "local": 134 | non_abi3_symbols.add(sym) 135 | except Exception as exc: 136 | raise AuditError(f"failed to collect symbols in shared object: {exc}") 137 | 138 | return AuditResult(so, baseline, computed, non_abi3_symbols, future_abi3_objects) 139 | -------------------------------------------------------------------------------- /abi3audit/_cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Caching middleware for `abi3audit`. 3 | """ 4 | 5 | from requests_cache import CachedSession 6 | 7 | 8 | def caching_session() -> CachedSession: 9 | """ 10 | Return a `requests` style session, with suitable caching middleware. 11 | """ 12 | 13 | session = CachedSession("abi3audit", use_cache_dir=True) 14 | session.max_redirects = 5 15 | return session 16 | -------------------------------------------------------------------------------- /abi3audit/_cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | The `abi3audit` CLI. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import argparse 8 | import json 9 | import logging 10 | import os 11 | import sys 12 | from collections import defaultdict 13 | from typing import Any 14 | 15 | from abi3info.models import PyVersion 16 | from rich import traceback 17 | from rich.logging import RichHandler 18 | 19 | from abi3audit import __version__ 20 | from abi3audit._audit import AuditError, AuditResult, audit 21 | from abi3audit._extract import ( 22 | Extractor, 23 | ExtractorError, 24 | InvalidSpec, 25 | PyPISpec, 26 | SharedObjectSpec, 27 | WheelSpec, 28 | make_specs, 29 | ) 30 | from abi3audit._object import SharedObject 31 | from abi3audit._state import console, status 32 | 33 | logger = logging.getLogger(__name__) 34 | logging.basicConfig( 35 | level=os.environ.get("ABI3AUDIT_LOGLEVEL", "INFO").upper(), 36 | format="%(message)s", 37 | datefmt="[%X]", 38 | handlers=[RichHandler(console=console)], 39 | ) 40 | 41 | traceback.install(show_locals=True) 42 | 43 | 44 | # TODO: Put these helpers somewhere else. 45 | def _green(s: str) -> str: 46 | return f"[green]{s}[/green]" 47 | 48 | 49 | def _yellow(s: str) -> str: 50 | return f"[yellow]{s}[/yellow]" 51 | 52 | 53 | def _red(s: str) -> str: 54 | return f"[red]{s}[/red]" 55 | 56 | 57 | def _colornum(n: int) -> str: 58 | if n == 0: 59 | return _green(str(n)) 60 | return _red(str(n)) 61 | 62 | 63 | class _PyVersionAction(argparse.Action): 64 | def __call__( 65 | self, 66 | parser: argparse.ArgumentParser, 67 | namespace: argparse.Namespace, 68 | values: Any, 69 | option_string: Any = None, 70 | ) -> None: 71 | try: 72 | pyversion = self._ensure_pyversion(values) 73 | except ValueError as exc: 74 | raise argparse.ArgumentError(self, str(exc)) 75 | setattr(namespace, self.dest, pyversion) 76 | 77 | @classmethod 78 | def _ensure_pyversion(cls, version: str) -> PyVersion: 79 | error_msg = f"must have syntax '3.x', with x>=2; you gave '{version}'" 80 | try: 81 | pyversion = PyVersion.parse_dotted(version) 82 | except Exception: 83 | raise ValueError(error_msg) 84 | if pyversion.major != 3 or pyversion.minor < 2: 85 | raise ValueError(error_msg) 86 | return pyversion 87 | 88 | 89 | class SpecResults: 90 | def __init__(self) -> None: 91 | # Map of extractor -> shared object -> audit result 92 | self._results: defaultdict[Extractor, list[AuditResult]] = defaultdict(list) 93 | self._bad_abi3_version_counts: defaultdict[SharedObject, int] = defaultdict(int) 94 | self._abi3_violation_counts: defaultdict[SharedObject, int] = defaultdict(int) 95 | 96 | def add(self, extractor: Extractor, so: SharedObject, result: AuditResult) -> None: 97 | self._results[extractor].append(result) 98 | 99 | if result.computed > result.baseline: 100 | self._bad_abi3_version_counts[so] += 1 101 | 102 | self._abi3_violation_counts[so] += len(result.non_abi3_symbols) 103 | 104 | def summarize_extraction(self, extractor: Extractor, summary: bool) -> str | None: 105 | spec_results = self._results[extractor] 106 | 107 | if not spec_results: 108 | return _yellow(f":person_shrugging: nothing auditable found in {extractor.spec}") 109 | 110 | abi3_version_counts = sum(self._bad_abi3_version_counts[res.so] for res in spec_results) 111 | abi3_violations = sum(self._abi3_violation_counts[res.so] for res in spec_results) 112 | if summary or abi3_violations > 0 or abi3_version_counts > 0: 113 | return ( 114 | f":information_desk_person: {extractor}: {len(spec_results)} extensions scanned; " 115 | f"{_colornum(abi3_version_counts)} ABI version mismatches and " 116 | f"{_colornum(abi3_violations)} ABI violations found" 117 | ) 118 | else: 119 | return None 120 | 121 | def json(self) -> dict[str, Any]: 122 | """ 123 | Returns a JSON-serializable dictionary representation of this `SpecResults`. 124 | """ 125 | 126 | # TODO(ww): These inner helpers could definitely be consolidated. 127 | def _one_object(results: list[AuditResult]) -> dict[str, Any]: 128 | # NOTE: Anything else indicates a logic error. 129 | assert len(results) == 1 130 | return {"name": results[0].so.path.name, "result": results[0].json()} 131 | 132 | def _one_wheel(results: list[AuditResult]) -> list[dict[str, Any]]: 133 | sos = [] 134 | for result in results: 135 | sos.append( 136 | { 137 | "name": result.so.path.name, 138 | "result": result.json(), 139 | } 140 | ) 141 | return sos 142 | 143 | def _one_package(results: list[AuditResult]) -> dict[str, Any]: 144 | sos_by_wheel = defaultdict(list) 145 | for result in results: 146 | # NOTE: mypy can't see that this is never None in this context. 147 | wheel_name = result.so._extractor.parent.path.name # type: ignore[union-attr] 148 | sos_by_wheel[wheel_name].append( 149 | { 150 | "name": result.so.path.name, 151 | "result": result.json(), 152 | } 153 | ) 154 | return sos_by_wheel 155 | 156 | def _one_extractor(extractor: Extractor, results: list[AuditResult]) -> dict[str, Any]: 157 | body: dict[str, Any] 158 | if isinstance(extractor.spec, WheelSpec): 159 | body = {"kind": "wheel", "wheel": _one_wheel(results)} 160 | elif isinstance(extractor.spec, SharedObjectSpec): 161 | body = {"kind": "object", "object": _one_object(results)} 162 | elif isinstance(extractor.spec, PyPISpec): 163 | body = {"kind": "package", "package": _one_package(results)} 164 | return body 165 | 166 | summary: dict[str, Any] = {} 167 | specs = summary["specs"] = {} 168 | for extractor, results in self._results.items(): 169 | specs[extractor.spec] = _one_extractor(extractor, results) 170 | 171 | return summary 172 | 173 | 174 | def main() -> None: 175 | parser = argparse.ArgumentParser( 176 | prog="abi3audit", 177 | description="Scans Python extensions for abi3 violations and inconsistencies", 178 | ) 179 | parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") 180 | parser.add_argument( 181 | "specs", 182 | metavar="SPEC", 183 | nargs="+", 184 | help="the files or other dependency specs to scan", 185 | ) 186 | parser.add_argument( 187 | "--debug", 188 | action="store_true", 189 | help=( 190 | "emit debug statements; this setting also overrides `ABI3AUDIT_LOGLEVEL` and " 191 | "is equivalent to setting it to `debug`" 192 | ), 193 | ) 194 | parser.add_argument( 195 | "-v", 196 | "--verbose", 197 | action="store_true", 198 | help=("give more output, including pretty-printed results for each audit step"), 199 | ) 200 | parser.add_argument( 201 | "-R", "--report", action="store_true", help="generate a JSON report; uses --output" 202 | ) 203 | parser.add_argument( 204 | "-o", 205 | "--output", 206 | type=argparse.FileType("w"), 207 | default=sys.stdout, 208 | help="the path to write the JSON report to (default: stdout)", 209 | ) 210 | parser.add_argument( 211 | "-s", 212 | "--summary", 213 | action="store_true", 214 | help="always output a summary even if there are no violations/ABI version mismatches", 215 | ) 216 | parser.add_argument( 217 | "-S", 218 | "--strict", 219 | action="store_true", 220 | help="fail the entire audit if an individual audit step fails", 221 | ) 222 | parser.add_argument( 223 | "--assume-minimum-abi3", 224 | action=_PyVersionAction, 225 | help="assumed abi3 version (3.x, with x>=2) if it cannot be detected", 226 | ) 227 | args = parser.parse_args() 228 | 229 | if args.debug: 230 | logging.root.setLevel("DEBUG") 231 | 232 | specs = [] 233 | for spec in args.specs: 234 | try: 235 | specs.extend(make_specs(spec, assume_minimum_abi3=args.assume_minimum_abi3)) 236 | except InvalidSpec as e: 237 | console.log(f"[red]:thumbs_down: processing error: {e}") 238 | sys.exit(1) 239 | 240 | logger.debug(f"parsed arguments: {args}") 241 | 242 | results = SpecResults() 243 | all_passed = True 244 | with status: 245 | for spec in specs: 246 | status.update(f"auditing {spec}") 247 | try: 248 | extractor = spec._extractor() 249 | except ExtractorError as e: 250 | console.log(f"[red]:thumbs_down: processing error: {e}") 251 | sys.exit(1) 252 | 253 | for so in extractor: 254 | status.update(f"{spec}: auditing {so}") 255 | 256 | try: 257 | result = audit(so, assume_minimum_abi3=args.assume_minimum_abi3) 258 | all_passed = all_passed and bool(result) 259 | except AuditError as exc: 260 | # TODO(ww): Refine exceptions and error states here. 261 | console.log(f"[red]:skull: {so}: auditing error: {exc}") 262 | if args.strict: 263 | sys.exit(1) 264 | continue 265 | 266 | results.add(extractor, so, result) 267 | if not result and args.verbose: 268 | console.log(result) 269 | 270 | log_message = results.summarize_extraction(extractor, args.summary or args.verbose) 271 | if log_message: 272 | console.log(log_message) 273 | 274 | if args.report: 275 | print(json.dumps(results.json()), file=args.output) 276 | 277 | if not all_passed: 278 | sys.exit(1) 279 | -------------------------------------------------------------------------------- /abi3audit/_extract.py: -------------------------------------------------------------------------------- 1 | """ 2 | Native extension extraction interfaces and implementations. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import glob 8 | import logging 9 | from collections.abc import Iterator 10 | from pathlib import Path 11 | from tempfile import TemporaryDirectory 12 | from typing import Any, Union 13 | from zipfile import ZipFile 14 | 15 | from abi3info.models import PyVersion 16 | from packaging import utils 17 | from packaging.requirements import InvalidRequirement, Requirement 18 | from packaging.tags import Tag 19 | 20 | import abi3audit._object as _object 21 | from abi3audit import __version__ 22 | from abi3audit._cache import caching_session 23 | from abi3audit._state import console, status 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | _SHARED_OBJECT_SUFFIXES = [".so", ".pyd"] 28 | 29 | 30 | def _glob_all_objects(path: Path) -> Iterator[Path]: 31 | # NOTE: abi3 extensions are normally tagged with .abi3.SUFFIX, 32 | # e.g. _foo.abi3.so. Experimentally however, not all are, so we 33 | # don't filter on the presence of that additional tag. 34 | for suffix in _SHARED_OBJECT_SUFFIXES: 35 | yield from path.glob(f"**/*{suffix}") 36 | 37 | 38 | class InvalidSpec(ValueError): 39 | """ 40 | Raised when abi3audit doesn't know how to convert a user's auditing 41 | specification into something that can be extracted. 42 | """ 43 | 44 | pass 45 | 46 | 47 | class WheelSpec(str): 48 | """ 49 | Represents a Python wheel, presumed to be present on disk. 50 | 51 | A wheel can contain multiple Python extensions, as shared objects. 52 | """ 53 | 54 | def _extractor(self) -> Extractor: 55 | """ 56 | Returns an extractor for this wheel's shared objects. 57 | """ 58 | return WheelExtractor(self) 59 | 60 | 61 | class SharedObjectSpec(str): 62 | """ 63 | Represents a single shared object, presumed to be present on disk. 64 | 65 | A shared object may or may not be a Python extension. 66 | """ 67 | 68 | def _extractor(self) -> Extractor: 69 | """ 70 | Returns a "trivial" extractor for this shared object. 71 | """ 72 | return SharedObjectExtractor(self) 73 | 74 | 75 | class PyPISpec(str): 76 | """ 77 | Represents a package on PyPI. 78 | 79 | A package may have zero or more published wheels, of which zero or more 80 | may be tagged as abi3 compatible. 81 | """ 82 | 83 | def _extractor(self) -> Extractor: 84 | """ 85 | Returns an extractor for each shared object in each published abi3 wheel. 86 | """ 87 | return PyPIExtractor(self) 88 | 89 | 90 | Spec = Union[WheelSpec, SharedObjectSpec, PyPISpec] 91 | 92 | 93 | def make_specs(val: str, assume_minimum_abi3: PyVersion | None = None) -> list[Spec]: 94 | """ 95 | Constructs a (minimally) valid list of `Spec` instances from the given input. 96 | """ 97 | if val.endswith(".whl"): 98 | # NOTE: Windows is notable for not supporting globs in its shells. 99 | # If the user specifies a glob, expand it for them. 100 | if "*" in val: 101 | return [WheelSpec(s) for s in glob.glob(val)] 102 | return [WheelSpec(val)] 103 | elif any(val.endswith(suf) for suf in _SHARED_OBJECT_SUFFIXES): 104 | # NOTE: We allow untagged shared objects when they're indirectly 105 | # audited (e.g. via an abi3 wheel), but when auditing them directly we 106 | # only allow them if we have a minimum abi3 version to check against. 107 | if assume_minimum_abi3 is None and ".abi3." not in val: 108 | raise InvalidSpec( 109 | "--assume-minimum-abi3 must be used when extension does not contain '.abi3.' infix" 110 | ) 111 | return [SharedObjectSpec(val)] 112 | else: 113 | try: 114 | _ = Requirement(val) 115 | return [PyPISpec(val)] 116 | except InvalidRequirement as e: 117 | raise InvalidSpec( 118 | f"'{val}' does not look like a valid wheel, shared object, or package name\n" 119 | f"hint: {e}" 120 | ) 121 | 122 | 123 | class ExtractorError(ValueError): 124 | """ 125 | Raised when abi3audit doesn't know how to (or can't) extract shared objects 126 | from the requested source. 127 | """ 128 | 129 | pass 130 | 131 | 132 | class WheelExtractor: 133 | """ 134 | An extractor for Python wheels. 135 | 136 | This extractor collects and yields each shared object in the specified wheel. 137 | """ 138 | 139 | def __init__(self, spec: WheelSpec, parent: PyPIExtractor | None = None) -> None: 140 | self.spec = spec 141 | self.path = Path(self.spec) 142 | self.parent = parent 143 | 144 | if not self.path.is_file(): 145 | raise ExtractorError(f"not a file: {self.path}") 146 | 147 | # TODO: Do this during initialization instead, so that we can turn 148 | # more things into early errors (like the wheel not being abi3-tagged). 149 | @property 150 | def tagset(self) -> frozenset[Tag]: 151 | return utils.parse_wheel_filename(self.path.name)[-1] 152 | 153 | def __iter__(self) -> Iterator[_object.SharedObject]: 154 | status.update(f"{self}: collecting shared objects") 155 | with TemporaryDirectory() as td, ZipFile(self.path, "r") as zf: 156 | exploded_path = Path(td) 157 | zf.extractall(exploded_path) 158 | 159 | for so_path in _glob_all_objects(exploded_path): 160 | child = SharedObjectExtractor(SharedObjectSpec(so_path), parent=self) 161 | yield from child 162 | 163 | def __str__(self) -> str: 164 | return self.path.name 165 | 166 | 167 | class SharedObjectExtractor: 168 | """ 169 | An extractor for shared objects. 170 | 171 | This extractor is "trivial", since it yields a shared object corresponding to 172 | the spec it created with. 173 | """ 174 | 175 | def __init__(self, spec: SharedObjectSpec, parent: WheelExtractor | None = None) -> None: 176 | self.spec = spec 177 | self.path = Path(self.spec) 178 | self.parent = parent 179 | 180 | if not self.path.is_file(): 181 | raise ExtractorError(f"not a file: {self.path}") 182 | 183 | def _elf_magic(self) -> bool: 184 | with self.path.open("rb") as io: 185 | magic = io.read(4) 186 | return magic == b"\x7fELF" 187 | 188 | def __iter__(self) -> Iterator[_object.SharedObject]: 189 | if self.path.suffix == ".so": 190 | # Python uses .so for extensions on macOS as well, rather 191 | # than the normal .dylib extension. As a result, we have to 192 | # suss out the underlying format from the wheel's tags, 193 | # or from the magic bytes as a last result. 194 | if ( 195 | self.parent 196 | and any("macosx" in t.platform for t in self.parent.tagset) 197 | or not self._elf_magic() 198 | ): 199 | yield _object._Dylib(self) 200 | else: 201 | yield _object._So(self) 202 | elif self.path.suffix == ".pyd": 203 | yield _object._Dll(self) 204 | 205 | def __str__(self) -> str: 206 | return self.path.name 207 | 208 | 209 | class PyPIExtractor: 210 | """ 211 | An extractor for packages published on PyPI. 212 | 213 | This extractor yields each shared object in each abi3-tagged wheel present 214 | on PyPI. 215 | """ 216 | 217 | def __init__(self, spec: PyPISpec) -> None: 218 | self.spec = spec 219 | self.parent = None 220 | self._session = caching_session() 221 | self._session.headers.update( 222 | {"Accept-Encoding": "gzip", "User-Agent": f"abi3audit/{__version__}"} 223 | ) 224 | 225 | def __iter__(self) -> Iterator[_object.SharedObject]: 226 | # if we get here, we already know it's a valid requirement. 227 | requirement = Requirement(self.spec) 228 | package = requirement.name 229 | specifier_set = requirement.specifier 230 | 231 | status.update(f"{self}: querying PyPI") 232 | 233 | # TODO: Error handling for this request. 234 | resp = self._session.get(f"https://pypi.org/pypi/{package}/json") 235 | 236 | if not resp.ok: 237 | console.log(f"[red]:skull: {self}: PyPI returned {resp.status_code}") 238 | yield from () 239 | return 240 | 241 | body = resp.json() 242 | 243 | status.update(f"{self}: collecting distributions from PyPI") 244 | releases: dict[str, Any] = body.get("releases") 245 | if not releases: 246 | console.log(f"[red]:confused: {self}: no releases on PyPI") 247 | yield from () 248 | return 249 | 250 | for v, dists in releases.items(): 251 | if v not in specifier_set: 252 | continue 253 | 254 | for dist in dists: 255 | # If it's not a wheel, we can't audit it. 256 | if not dist["filename"].endswith(".whl"): 257 | continue 258 | 259 | # If it's not an abi3 wheel, we don't have anything interesting 260 | # to say about it. 261 | # TODO: Maybe include non-abi3 wheels so that we can detect 262 | # wheels that can be safely marked as abi3? 263 | # 264 | # NOTE: Unfortunately, there are some wheels on PyPI that don't 265 | # have valid (PEP427) filenames, like: 266 | # 267 | # pyffmpeg-2.0.5-cp35.cp36.cp37.cp38.cp39-macosx_10_14_x86_64.whl 268 | # 269 | # ...which is missing an ABI tag. 270 | # 271 | # There's not much we can do about these other than fail in 272 | # a controlled fashion and warn the user. 273 | try: 274 | tagset = utils.parse_wheel_filename(dist["filename"])[-1] 275 | except utils.InvalidWheelFilename as exc: 276 | console.log(f"[red]:skull: {self}: {exc}") 277 | continue 278 | 279 | if not any(t.abi == "abi3" for t in tagset): 280 | logger.debug(f"skipping non-abi3 wheel: {dist['filename']}") 281 | continue 282 | 283 | status.update(f"{self}: {dist['filename']}: retrieving wheel") 284 | resp = self._session.get(dist["url"]) 285 | with TemporaryDirectory() as td: 286 | wheel_path = Path(td) / dist["filename"] 287 | wheel_path.write_bytes(resp.content) 288 | 289 | child = WheelExtractor(WheelSpec(wheel_path), parent=self) 290 | yield from child 291 | 292 | def __str__(self) -> str: 293 | return self.spec 294 | 295 | 296 | Extractor = Union[WheelExtractor, SharedObjectExtractor, PyPIExtractor] 297 | -------------------------------------------------------------------------------- /abi3audit/_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | Models and logic for handling different types of shared objects. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import logging 8 | import struct 9 | from collections.abc import Iterator 10 | from typing import Any, Union 11 | 12 | import pefile 13 | from abi3info.models import PyVersion, Symbol, Visibility 14 | from elftools.elf.elffile import ELFFile 15 | 16 | import abi3audit._extract as extract 17 | from abi3audit._vendor import mach_o 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class SharedObjectError(Exception): 23 | pass 24 | 25 | 26 | class _SharedObjectBase: 27 | """ 28 | A mixin for common behavior between all types of shared objects. 29 | """ 30 | 31 | def __init__(self, extractor: extract.SharedObjectExtractor): 32 | self._extractor = extractor 33 | self.path = self._extractor.path 34 | 35 | def abi3_version(self, assume_lowest: PyVersion) -> PyVersion | None: 36 | # If we're dealing with a shared object that was extracted from a wheel, 37 | # we try and suss out the abi3 version from the wheel's own tags. 38 | if self._extractor.parent is not None: 39 | # Multiple tagsets can be marked as abi3; when this happens, 40 | # we select the highest interpreter version. 41 | tagset = self._extractor.parent.tagset 42 | pyversions = [ 43 | PyVersion.parse_python_tag(t.interpreter) for t in tagset if t.abi == "abi3" 44 | ] 45 | if len(pyversions) > 0: 46 | return max(pyversions) 47 | 48 | # If we're dealing with a standalone shared object (or the above fell through), 49 | # we fall back on checking for the ".abi3" marker in the shared object's own 50 | # filename. This doesn't tell us anything about the specific abi3 version, so 51 | # we assume the lowest. 52 | if ".abi3" in self._extractor.path.suffixes: 53 | if assume_lowest is None: 54 | assume_lowest = PyVersion(3, 2) 55 | 56 | logger.debug( 57 | "no wheel to infer abi3 version from; assuming (%s)", 58 | assume_lowest, 59 | ) 60 | return assume_lowest 61 | 62 | # With no wheel tags and no filename tag, fall back on the assumed ABI 63 | # version (which is possibly None). 64 | return assume_lowest 65 | 66 | def __str__(self) -> str: 67 | parents = [] 68 | current = self._extractor.parent 69 | while current is not None: 70 | parents.append(str(current)) 71 | current = current.parent # type: ignore[assignment] 72 | return f"{': '.join(reversed(parents))}: {self._extractor}" 73 | 74 | 75 | class _So(_SharedObjectBase): 76 | """ 77 | An ELF-formatted shared object. 78 | """ 79 | 80 | def __init__(self, extractor: extract.SharedObjectExtractor): 81 | super().__init__(extractor) 82 | 83 | with self._extractor.path.open(mode="rb") as io, ELFFile(io) as elf: 84 | # HACK: If the ELF contains a comment section, look for an indicator 85 | # that the binary was produced by GCC. If so, we treat STB_LOOS 86 | # as STB_GNU_UNIQUE, i.e. another kind of local symbol linkage. 87 | comment = elf.get_section_by_name(".comment") 88 | if comment is None: 89 | logger.debug(f"{self._extractor.path} has no .comment") 90 | self._loos_is_gnu_unique = False 91 | else: 92 | self._loos_is_gnu_unique = b"GCC" in comment.data() 93 | logger.debug(f"{self._extractor.path} has .comment, {self._loos_is_gnu_unique=}") 94 | 95 | def __iter__(self) -> Iterator[Symbol]: 96 | def get_visibility(_sym: Any) -> Visibility | None: 97 | elfviz: str = _sym.entry.st_info.bind 98 | if elfviz == "STB_LOCAL": 99 | return "local" 100 | elif elfviz == "STB_LOOS" and self._loos_is_gnu_unique: 101 | return "local" 102 | elif elfviz == "STB_GLOBAL": 103 | return "global" 104 | elif elfviz == "STB_WEAK": 105 | return "weak" 106 | else: 107 | logger.warning(f"unexpected ELF visibility value {elfviz!r}") 108 | return None 109 | 110 | with self._extractor.path.open(mode="rb") as io, ELFFile(io) as elf: 111 | # The dynamic linker on Linux uses .dynsym, not .symtab, for 112 | # link editing and relocation. However, an extension that was 113 | # compiled as non-abi3 might have CPython functions inlined into 114 | # it, and we'd like to detect those. As such, we scan both symbol 115 | # tables. 116 | symtab = elf.get_section_by_name(".symtab") 117 | if symtab is not None: 118 | for sym in symtab.iter_symbols(): 119 | yield Symbol(sym.name, visibility=get_visibility(sym)) 120 | 121 | dynsym = elf.get_section_by_name(".dynsym") 122 | if dynsym is not None: 123 | for sym in dynsym.iter_symbols(): 124 | yield Symbol(sym.name, visibility=get_visibility(sym)) 125 | 126 | 127 | class _Dylib(_SharedObjectBase): 128 | """ 129 | A Mach-O-formatted macOS dynamic library. 130 | """ 131 | 132 | def _each_macho(self) -> Iterator[mach_o.MachO]: 133 | # This Mach-O parser doesn't currently support "fat" Mach-Os, where 134 | # multiple single-arch Mach-Os are embedded as slices. 135 | try: 136 | with mach_o.MachO.from_file(self._extractor.path) as macho: 137 | yield macho 138 | except Exception: 139 | logger.debug(f"mach-o decode for {self._extractor.path} failed; trying as a fat mach-o") 140 | # To handle "fat" Mach-Os, we do some ad-hoc parsing below: 141 | # * Check that we're really in a fat Mach-O and, if 142 | # we are, figure out whether it's a 32-bit or 64-bit style one; 143 | # * Figure out how many architecture slices (nfat_arch) there are; 144 | # * Loop over each, grabbing the inner Mach-O's file offset and size; 145 | # * Passing the raw data at that offset to the "thin" Mach-O parser 146 | macho_slices = [] 147 | with self._extractor.path.open("rb") as io: 148 | (magic,) = struct.unpack(">I", io.read(4)) 149 | if magic == 0xCAFEBABE: 150 | fieldspec = (">I", 4) 151 | elif magic == 0xCAFEBABF: 152 | fieldspec = (">Q", 8) 153 | else: 154 | # NOTE: There are technically two other magics for little-endian 155 | # Mach-O Fat headers, but they never appear in the wild. 156 | raise SharedObjectError( 157 | f"bad magic: {hex(magic)} (not FAT_MAGIC or FAT_MAGIC_64)" 158 | ) 159 | 160 | (nfat_arch,) = struct.unpack(fieldspec[0], io.read(fieldspec[1])) 161 | for _ in range(nfat_arch): 162 | # Move past cputype and cpusubtype (both uint32) 163 | _ = io.read(8) 164 | (mach_offset,) = struct.unpack(fieldspec[0], io.read(fieldspec[1])) 165 | (mach_size,) = struct.unpack(fieldspec[0], io.read(fieldspec[1])) 166 | macho_slices.append((mach_offset, mach_size)) 167 | # Move past align (uint32) and, if it's 64-bit, reserved (uint32) as well 168 | _ = io.read(4) 169 | if fieldspec[1] == 8: 170 | _ = io.read(4) 171 | 172 | # Finally, parse each Mach-O. 173 | logger.debug(f"fat macho: identified {nfat_arch} mach-o slices: {macho_slices}") 174 | for offset, size in macho_slices: 175 | io.seek(offset) 176 | raw_macho = io.read(size) 177 | 178 | with mach_o.MachO.from_bytes(raw_macho) as macho: 179 | yield macho 180 | 181 | def __iter__(self) -> Iterator[Symbol]: 182 | def get_visibility(_macho_sym: Any) -> Visibility: 183 | # N_TYPE is the bitmask giving the Mach-O symbol type. 184 | # Possible values are: 185 | # 0x0 - symbol undefined, missing section field. 186 | # 0x2 - symbol absolute, missing section field. 187 | # 0xe - symbol defined in section type given by _macho_sym.sect 188 | # 0xc - symbol undefined w/ prebound value, missing section field. 189 | # 0xa - symbol is the same as the one found in table at index _macho_sym.value. 190 | # (See https://github.com/aidansteele/osx-abi-macho-file-format-reference/blob/master/README.md#nlist_64) 191 | # We assume that if the N_TYPE is 0xe, the linkage is internal, 192 | # and that everything else comes from a shared library. 193 | N_TYPE = 0xE 194 | if _macho_sym.type & N_TYPE == N_TYPE: 195 | return "local" 196 | return "global" 197 | 198 | for macho in self._each_macho(): 199 | symtab_cmd = next( 200 | ( 201 | lc.body 202 | for lc in macho.load_commands 203 | if lc.type == mach_o.MachO.LoadCommandType.symtab 204 | ), 205 | None, 206 | ) 207 | if symtab_cmd is None: 208 | raise SharedObjectError("shared object has no symbol table") 209 | 210 | N_EXT = 0x01 211 | for symbol in symtab_cmd.symbols: 212 | # The Mach-O symbol table includes all kinds of junk, including 213 | # symbolic entries for debuggers. We exclude all of 214 | # these non-function/data entries, as well as any symbols 215 | # that are not marked as external (since we're linking against 216 | # the Python interpreter for the ABI). 217 | # A symbol is marked as external if the N_EXT (rightmost) bit 218 | # of its `type` attribute is set. 219 | if (name := symbol.name) is None or (symbol.type & N_EXT != N_EXT): 220 | continue 221 | 222 | # All symbols on macOS are prefixed with _; remove it. 223 | yield Symbol(name[1:], visibility=get_visibility(symbol)) 224 | 225 | 226 | class _Dll(_SharedObjectBase): 227 | """ 228 | A PE-formatted Windows DLL. 229 | """ 230 | 231 | def __iter__(self) -> Iterator[Symbol]: 232 | with pefile.PE(self._extractor.path) as pe: 233 | pe.parse_data_directories() 234 | for import_data in pe.DIRECTORY_ENTRY_IMPORT: 235 | for imp in import_data.imports: 236 | # TODO(ww): Root-cause this; imports should always be named 237 | # (in my understanding of import tables in PE). 238 | if imp.name is None: 239 | logger.debug(f"weird: skipping import data entry without name: {imp}") 240 | continue 241 | # On Windows, all present symbols are also global - 242 | # that is, __declspec(dllexport) is equal to -fvisibility=hidden 243 | # plus __attribute__((visibility(default))). 244 | yield Symbol(imp.name.decode(), visibility="global") 245 | 246 | 247 | SharedObject = Union[_So, _Dll, _Dylib] 248 | -------------------------------------------------------------------------------- /abi3audit/_state.py: -------------------------------------------------------------------------------- 1 | """ 2 | `abi3audit` CLI state, broken out to avoid circular imports. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import os 8 | import sys 9 | from typing import Literal 10 | 11 | from rich.console import Console 12 | 13 | # TODO: Remove this once rich's NO_COLOR handling is fixed. 14 | # See: https://github.com/Textualize/rich/issues/2549 15 | _color_system: Literal["auto"] | None 16 | if os.getenv("NO_COLOR", None) is not None: 17 | _color_system = None 18 | else: 19 | _color_system = "auto" 20 | 21 | console = Console(log_path=False, file=sys.stderr, color_system=_color_system) 22 | status = console.status("[green]Processing inputs", spinner="clock") 23 | -------------------------------------------------------------------------------- /abi3audit/_vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/abi3audit/bb0a20730e2dd39524de14b34ff63e72224b40c7/abi3audit/_vendor/__init__.py -------------------------------------------------------------------------------- /abi3audit/_vendor/asn1_der.py: -------------------------------------------------------------------------------- 1 | # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | import kaitaistruct 4 | from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO 5 | from enum import Enum 6 | 7 | 8 | if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): 9 | raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) 10 | 11 | class Asn1Der(KaitaiStruct): 12 | """ASN.1 (Abstract Syntax Notation One) DER (Distinguished Encoding 13 | Rules) is a standard-backed serialization scheme used in many 14 | different use-cases. Particularly popular usage scenarios are X.509 15 | certificates and some telecommunication / networking protocols. 16 | 17 | DER is self-describing encoding scheme which allows representation 18 | of simple, atomic data elements, such as strings and numbers, and 19 | complex objects, such as sequences of other elements. 20 | 21 | DER is a subset of BER (Basic Encoding Rules), with an emphasis on 22 | being non-ambiguous: there's always exactly one canonical way to 23 | encode a data structure defined in terms of ASN.1 using DER. 24 | 25 | This spec allows full parsing of format syntax, but to understand 26 | the semantics, one would typically require a dictionary of Object 27 | Identifiers (OIDs), to match OID bodies against some human-readable 28 | list of constants. OIDs are covered by many different standards, 29 | so typically it's simpler to use a pre-compiled list of them, such 30 | as: 31 | 32 | * https://www.cs.auckland.ac.nz/~pgut001/dumpasn1.cfg 33 | * http://oid-info.com/ 34 | * https://www.alvestrand.no/objectid/top.html 35 | 36 | .. seealso:: 37 | Source - https://www.itu.int/itu-t/recommendations/rec.aspx?rec=12483&lang=en 38 | """ 39 | 40 | class TypeTag(Enum): 41 | end_of_content = 0 42 | boolean = 1 43 | integer = 2 44 | bit_string = 3 45 | octet_string = 4 46 | null_value = 5 47 | object_id = 6 48 | object_descriptor = 7 49 | external = 8 50 | real = 9 51 | enumerated = 10 52 | embedded_pdv = 11 53 | utf8string = 12 54 | relative_oid = 13 55 | sequence_10 = 16 56 | printable_string = 19 57 | ia5string = 22 58 | sequence_30 = 48 59 | set = 49 60 | def __init__(self, _io, _parent=None, _root=None): 61 | self._io = _io 62 | self._parent = _parent 63 | self._root = _root if _root else self 64 | self._read() 65 | 66 | def _read(self): 67 | self.type_tag = KaitaiStream.resolve_enum(Asn1Der.TypeTag, self._io.read_u1()) 68 | self.len = Asn1Der.LenEncoded(self._io, self, self._root) 69 | _on = self.type_tag 70 | if _on == Asn1Der.TypeTag.printable_string: 71 | self._raw_body = self._io.read_bytes(self.len.result) 72 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 73 | self.body = Asn1Der.BodyPrintableString(_io__raw_body, self, self._root) 74 | elif _on == Asn1Der.TypeTag.sequence_10: 75 | self._raw_body = self._io.read_bytes(self.len.result) 76 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 77 | self.body = Asn1Der.BodySequence(_io__raw_body, self, self._root) 78 | elif _on == Asn1Der.TypeTag.set: 79 | self._raw_body = self._io.read_bytes(self.len.result) 80 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 81 | self.body = Asn1Der.BodySequence(_io__raw_body, self, self._root) 82 | elif _on == Asn1Der.TypeTag.sequence_30: 83 | self._raw_body = self._io.read_bytes(self.len.result) 84 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 85 | self.body = Asn1Der.BodySequence(_io__raw_body, self, self._root) 86 | elif _on == Asn1Der.TypeTag.utf8string: 87 | self._raw_body = self._io.read_bytes(self.len.result) 88 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 89 | self.body = Asn1Der.BodyUtf8string(_io__raw_body, self, self._root) 90 | elif _on == Asn1Der.TypeTag.object_id: 91 | self._raw_body = self._io.read_bytes(self.len.result) 92 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 93 | self.body = Asn1Der.BodyObjectId(_io__raw_body, self, self._root) 94 | else: 95 | self.body = self._io.read_bytes(self.len.result) 96 | 97 | class BodySequence(KaitaiStruct): 98 | def __init__(self, _io, _parent=None, _root=None): 99 | self._io = _io 100 | self._parent = _parent 101 | self._root = _root if _root else self 102 | self._read() 103 | 104 | def _read(self): 105 | self.entries = [] 106 | i = 0 107 | while not self._io.is_eof(): 108 | self.entries.append(Asn1Der(self._io)) 109 | i += 1 110 | 111 | 112 | 113 | class BodyUtf8string(KaitaiStruct): 114 | def __init__(self, _io, _parent=None, _root=None): 115 | self._io = _io 116 | self._parent = _parent 117 | self._root = _root if _root else self 118 | self._read() 119 | 120 | def _read(self): 121 | self.str = (self._io.read_bytes_full()).decode(u"UTF-8") 122 | 123 | 124 | class BodyObjectId(KaitaiStruct): 125 | """ 126 | .. seealso:: 127 | Source - https://docs.microsoft.com/en-us/windows/desktop/SecCertEnroll/about-object-identifier 128 | """ 129 | def __init__(self, _io, _parent=None, _root=None): 130 | self._io = _io 131 | self._parent = _parent 132 | self._root = _root if _root else self 133 | self._read() 134 | 135 | def _read(self): 136 | self.first_and_second = self._io.read_u1() 137 | self.rest = self._io.read_bytes_full() 138 | 139 | @property 140 | def first(self): 141 | if hasattr(self, '_m_first'): 142 | return self._m_first 143 | 144 | self._m_first = self.first_and_second // 40 145 | return getattr(self, '_m_first', None) 146 | 147 | @property 148 | def second(self): 149 | if hasattr(self, '_m_second'): 150 | return self._m_second 151 | 152 | self._m_second = (self.first_and_second % 40) 153 | return getattr(self, '_m_second', None) 154 | 155 | 156 | class LenEncoded(KaitaiStruct): 157 | def __init__(self, _io, _parent=None, _root=None): 158 | self._io = _io 159 | self._parent = _parent 160 | self._root = _root if _root else self 161 | self._read() 162 | 163 | def _read(self): 164 | self.b1 = self._io.read_u1() 165 | if self.b1 == 130: 166 | self.int2 = self._io.read_u2be() 167 | 168 | if self.b1 == 129: 169 | self.int1 = self._io.read_u1() 170 | 171 | 172 | @property 173 | def result(self): 174 | if hasattr(self, '_m_result'): 175 | return self._m_result 176 | 177 | self._m_result = (self.int1 if self.b1 == 129 else (self.int2 if self.b1 == 130 else self.b1)) 178 | return getattr(self, '_m_result', None) 179 | 180 | 181 | class BodyPrintableString(KaitaiStruct): 182 | def __init__(self, _io, _parent=None, _root=None): 183 | self._io = _io 184 | self._parent = _parent 185 | self._root = _root if _root else self 186 | self._read() 187 | 188 | def _read(self): 189 | self.str = (self._io.read_bytes_full()).decode(u"ASCII") 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /abi3audit/_vendor/mach_o.py: -------------------------------------------------------------------------------- 1 | # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | import kaitaistruct 4 | from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO 5 | from enum import Enum 6 | 7 | 8 | if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): 9 | raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) 10 | 11 | from . import asn1_der 12 | class MachO(KaitaiStruct): 13 | 14 | class MagicType(Enum): 15 | fat_le = 3199925962 16 | fat_be = 3405691582 17 | macho_le_x86 = 3472551422 18 | macho_le_x64 = 3489328638 19 | macho_be_x86 = 4277009102 20 | macho_be_x64 = 4277009103 21 | 22 | class CpuType(Enum): 23 | vax = 1 24 | romp = 2 25 | ns32032 = 4 26 | ns32332 = 5 27 | i386 = 7 28 | mips = 8 29 | ns32532 = 9 30 | hppa = 11 31 | arm = 12 32 | mc88000 = 13 33 | sparc = 14 34 | i860 = 15 35 | i860_little = 16 36 | rs6000 = 17 37 | powerpc = 18 38 | abi64 = 16777216 39 | x86_64 = 16777223 40 | arm64 = 16777228 41 | powerpc64 = 16777234 42 | any = 4294967295 43 | 44 | class FileType(Enum): 45 | object = 1 46 | execute = 2 47 | fvmlib = 3 48 | core = 4 49 | preload = 5 50 | dylib = 6 51 | dylinker = 7 52 | bundle = 8 53 | dylib_stub = 9 54 | dsym = 10 55 | kext_bundle = 11 56 | 57 | class LoadCommandType(Enum): 58 | segment = 1 59 | symtab = 2 60 | symseg = 3 61 | thread = 4 62 | unix_thread = 5 63 | load_fvm_lib = 6 64 | id_fvm_lib = 7 65 | ident = 8 66 | fvm_file = 9 67 | prepage = 10 68 | dysymtab = 11 69 | load_dylib = 12 70 | id_dylib = 13 71 | load_dylinker = 14 72 | id_dylinker = 15 73 | prebound_dylib = 16 74 | routines = 17 75 | sub_framework = 18 76 | sub_umbrella = 19 77 | sub_client = 20 78 | sub_library = 21 79 | twolevel_hints = 22 80 | prebind_cksum = 23 81 | segment_64 = 25 82 | routines_64 = 26 83 | uuid = 27 84 | code_signature = 29 85 | segment_split_info = 30 86 | lazy_load_dylib = 32 87 | encryption_info = 33 88 | dyld_info = 34 89 | version_min_macosx = 36 90 | version_min_iphoneos = 37 91 | function_starts = 38 92 | dyld_environment = 39 93 | data_in_code = 41 94 | source_version = 42 95 | dylib_code_sign_drs = 43 96 | encryption_info_64 = 44 97 | linker_option = 45 98 | linker_optimization_hint = 46 99 | version_min_tvos = 47 100 | version_min_watchos = 48 101 | build_version = 50 102 | req_dyld = 2147483648 103 | load_weak_dylib = 2147483672 104 | rpath = 2147483676 105 | reexport_dylib = 2147483679 106 | dyld_info_only = 2147483682 107 | load_upward_dylib = 2147483683 108 | main = 2147483688 109 | def __init__(self, _io, _parent=None, _root=None): 110 | self._io = _io 111 | self._parent = _parent 112 | self._root = _root if _root else self 113 | self._read() 114 | 115 | def _read(self): 116 | self.magic = KaitaiStream.resolve_enum(MachO.MagicType, self._io.read_u4be()) 117 | self.header = MachO.MachHeader(self._io, self, self._root) 118 | self.load_commands = [] 119 | for i in range(self.header.ncmds): 120 | self.load_commands.append(MachO.LoadCommand(self._io, self, self._root)) 121 | 122 | 123 | class RpathCommand(KaitaiStruct): 124 | def __init__(self, _io, _parent=None, _root=None): 125 | self._io = _io 126 | self._parent = _parent 127 | self._root = _root if _root else self 128 | self._read() 129 | 130 | def _read(self): 131 | self.path_offset = self._io.read_u4le() 132 | self.path = (self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8") 133 | 134 | 135 | class Uleb128(KaitaiStruct): 136 | def __init__(self, _io, _parent=None, _root=None): 137 | self._io = _io 138 | self._parent = _parent 139 | self._root = _root if _root else self 140 | self._read() 141 | 142 | def _read(self): 143 | self.b1 = self._io.read_u1() 144 | if (self.b1 & 128) != 0: 145 | self.b2 = self._io.read_u1() 146 | 147 | if (self.b2 & 128) != 0: 148 | self.b3 = self._io.read_u1() 149 | 150 | if (self.b3 & 128) != 0: 151 | self.b4 = self._io.read_u1() 152 | 153 | if (self.b4 & 128) != 0: 154 | self.b5 = self._io.read_u1() 155 | 156 | if (self.b5 & 128) != 0: 157 | self.b6 = self._io.read_u1() 158 | 159 | if (self.b6 & 128) != 0: 160 | self.b7 = self._io.read_u1() 161 | 162 | if (self.b7 & 128) != 0: 163 | self.b8 = self._io.read_u1() 164 | 165 | if (self.b8 & 128) != 0: 166 | self.b9 = self._io.read_u1() 167 | 168 | if (self.b9 & 128) != 0: 169 | self.b10 = self._io.read_u1() 170 | 171 | 172 | @property 173 | def value(self): 174 | if hasattr(self, '_m_value'): 175 | return self._m_value 176 | 177 | self._m_value = (((self.b1 % 128) << 0) + (0 if (self.b1 & 128) == 0 else (((self.b2 % 128) << 7) + (0 if (self.b2 & 128) == 0 else (((self.b3 % 128) << 14) + (0 if (self.b3 & 128) == 0 else (((self.b4 % 128) << 21) + (0 if (self.b4 & 128) == 0 else (((self.b5 % 128) << 28) + (0 if (self.b5 & 128) == 0 else (((self.b6 % 128) << 35) + (0 if (self.b6 & 128) == 0 else (((self.b7 % 128) << 42) + (0 if (self.b7 & 128) == 0 else (((self.b8 % 128) << 49) + (0 if (self.b8 & 128) == 0 else (((self.b9 % 128) << 56) + (0 if (self.b8 & 128) == 0 else ((self.b10 % 128) << 63))))))))))))))))))) 178 | return getattr(self, '_m_value', None) 179 | 180 | 181 | class SourceVersionCommand(KaitaiStruct): 182 | def __init__(self, _io, _parent=None, _root=None): 183 | self._io = _io 184 | self._parent = _parent 185 | self._root = _root if _root else self 186 | self._read() 187 | 188 | def _read(self): 189 | self.version = self._io.read_u8le() 190 | 191 | 192 | class CsBlob(KaitaiStruct): 193 | 194 | class CsMagic(Enum): 195 | blob_wrapper = 4208855809 196 | requirement = 4208856064 197 | requirements = 4208856065 198 | code_directory = 4208856066 199 | embedded_signature = 4208856256 200 | detached_signature = 4208856257 201 | entitlements = 4208882033 202 | der_entitlements = 4208882034 203 | def __init__(self, _io, _parent=None, _root=None): 204 | self._io = _io 205 | self._parent = _parent 206 | self._root = _root if _root else self 207 | self._read() 208 | 209 | def _read(self): 210 | self.magic = KaitaiStream.resolve_enum(MachO.CsBlob.CsMagic, self._io.read_u4be()) 211 | self.length = self._io.read_u4be() 212 | _on = self.magic 213 | if _on == MachO.CsBlob.CsMagic.requirement: 214 | self._raw_body = self._io.read_bytes((self.length - 8)) 215 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 216 | self.body = MachO.CsBlob.Requirement(_io__raw_body, self, self._root) 217 | elif _on == MachO.CsBlob.CsMagic.code_directory: 218 | self._raw_body = self._io.read_bytes((self.length - 8)) 219 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 220 | self.body = MachO.CsBlob.CodeDirectory(_io__raw_body, self, self._root) 221 | elif _on == MachO.CsBlob.CsMagic.requirements: 222 | self._raw_body = self._io.read_bytes((self.length - 8)) 223 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 224 | self.body = MachO.CsBlob.Requirements(_io__raw_body, self, self._root) 225 | elif _on == MachO.CsBlob.CsMagic.blob_wrapper: 226 | self._raw_body = self._io.read_bytes((self.length - 8)) 227 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 228 | self.body = MachO.CsBlob.BlobWrapper(_io__raw_body, self, self._root) 229 | elif _on == MachO.CsBlob.CsMagic.embedded_signature: 230 | self._raw_body = self._io.read_bytes((self.length - 8)) 231 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 232 | self.body = MachO.CsBlob.SuperBlob(_io__raw_body, self, self._root) 233 | elif _on == MachO.CsBlob.CsMagic.entitlements: 234 | self._raw_body = self._io.read_bytes((self.length - 8)) 235 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 236 | self.body = MachO.CsBlob.Entitlements(_io__raw_body, self, self._root) 237 | elif _on == MachO.CsBlob.CsMagic.detached_signature: 238 | self._raw_body = self._io.read_bytes((self.length - 8)) 239 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 240 | self.body = MachO.CsBlob.SuperBlob(_io__raw_body, self, self._root) 241 | elif _on == MachO.CsBlob.CsMagic.der_entitlements: 242 | self._raw_body = self._io.read_bytes((self.length - 8)) 243 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 244 | self.body = asn1_der.Asn1Der(_io__raw_body) 245 | else: 246 | self.body = self._io.read_bytes((self.length - 8)) 247 | 248 | class CodeDirectory(KaitaiStruct): 249 | def __init__(self, _io, _parent=None, _root=None): 250 | self._io = _io 251 | self._parent = _parent 252 | self._root = _root if _root else self 253 | self._read() 254 | 255 | def _read(self): 256 | self.version = self._io.read_u4be() 257 | self.flags = self._io.read_u4be() 258 | self.hash_offset = self._io.read_u4be() 259 | self.ident_offset = self._io.read_u4be() 260 | self.n_special_slots = self._io.read_u4be() 261 | self.n_code_slots = self._io.read_u4be() 262 | self.code_limit = self._io.read_u4be() 263 | self.hash_size = self._io.read_u1() 264 | self.hash_type = self._io.read_u1() 265 | self.spare1 = self._io.read_u1() 266 | self.page_size = self._io.read_u1() 267 | self.spare2 = self._io.read_u4be() 268 | if self.version >= 131328: 269 | self.scatter_offset = self._io.read_u4be() 270 | 271 | if self.version >= 131584: 272 | self.team_id_offset = self._io.read_u4be() 273 | 274 | 275 | @property 276 | def ident(self): 277 | if hasattr(self, '_m_ident'): 278 | return self._m_ident 279 | 280 | _pos = self._io.pos() 281 | self._io.seek((self.ident_offset - 8)) 282 | self._m_ident = (self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8") 283 | self._io.seek(_pos) 284 | return getattr(self, '_m_ident', None) 285 | 286 | @property 287 | def team_id(self): 288 | if hasattr(self, '_m_team_id'): 289 | return self._m_team_id 290 | 291 | _pos = self._io.pos() 292 | self._io.seek((self.team_id_offset - 8)) 293 | self._m_team_id = (self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8") 294 | self._io.seek(_pos) 295 | return getattr(self, '_m_team_id', None) 296 | 297 | @property 298 | def hashes(self): 299 | if hasattr(self, '_m_hashes'): 300 | return self._m_hashes 301 | 302 | _pos = self._io.pos() 303 | self._io.seek(((self.hash_offset - 8) - (self.hash_size * self.n_special_slots))) 304 | self._m_hashes = [] 305 | for i in range((self.n_special_slots + self.n_code_slots)): 306 | self._m_hashes.append(self._io.read_bytes(self.hash_size)) 307 | 308 | self._io.seek(_pos) 309 | return getattr(self, '_m_hashes', None) 310 | 311 | 312 | class Data(KaitaiStruct): 313 | def __init__(self, _io, _parent=None, _root=None): 314 | self._io = _io 315 | self._parent = _parent 316 | self._root = _root if _root else self 317 | self._read() 318 | 319 | def _read(self): 320 | self.length = self._io.read_u4be() 321 | self.value = self._io.read_bytes(self.length) 322 | self.padding = self._io.read_bytes((4 - (self.length & 3))) 323 | 324 | 325 | class SuperBlob(KaitaiStruct): 326 | def __init__(self, _io, _parent=None, _root=None): 327 | self._io = _io 328 | self._parent = _parent 329 | self._root = _root if _root else self 330 | self._read() 331 | 332 | def _read(self): 333 | self.count = self._io.read_u4be() 334 | self.blobs = [] 335 | for i in range(self.count): 336 | self.blobs.append(MachO.CsBlob.BlobIndex(self._io, self, self._root)) 337 | 338 | 339 | 340 | class Expr(KaitaiStruct): 341 | 342 | class OpEnum(Enum): 343 | false = 0 344 | true = 1 345 | ident = 2 346 | apple_anchor = 3 347 | anchor_hash = 4 348 | info_key_value = 5 349 | and_op = 6 350 | or_op = 7 351 | cd_hash = 8 352 | not_op = 9 353 | info_key_field = 10 354 | cert_field = 11 355 | trusted_cert = 12 356 | trusted_certs = 13 357 | cert_generic = 14 358 | apple_generic_anchor = 15 359 | entitlement_field = 16 360 | 361 | class CertSlot(Enum): 362 | left_cert = 0 363 | anchor_cert = 4294967295 364 | def __init__(self, _io, _parent=None, _root=None): 365 | self._io = _io 366 | self._parent = _parent 367 | self._root = _root if _root else self 368 | self._read() 369 | 370 | def _read(self): 371 | self.op = KaitaiStream.resolve_enum(MachO.CsBlob.Expr.OpEnum, self._io.read_u4be()) 372 | _on = self.op 373 | if _on == MachO.CsBlob.Expr.OpEnum.ident: 374 | self.data = MachO.CsBlob.Expr.IdentExpr(self._io, self, self._root) 375 | elif _on == MachO.CsBlob.Expr.OpEnum.or_op: 376 | self.data = MachO.CsBlob.Expr.OrExpr(self._io, self, self._root) 377 | elif _on == MachO.CsBlob.Expr.OpEnum.info_key_value: 378 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 379 | elif _on == MachO.CsBlob.Expr.OpEnum.anchor_hash: 380 | self.data = MachO.CsBlob.Expr.AnchorHashExpr(self._io, self, self._root) 381 | elif _on == MachO.CsBlob.Expr.OpEnum.info_key_field: 382 | self.data = MachO.CsBlob.Expr.InfoKeyFieldExpr(self._io, self, self._root) 383 | elif _on == MachO.CsBlob.Expr.OpEnum.not_op: 384 | self.data = MachO.CsBlob.Expr(self._io, self, self._root) 385 | elif _on == MachO.CsBlob.Expr.OpEnum.entitlement_field: 386 | self.data = MachO.CsBlob.Expr.EntitlementFieldExpr(self._io, self, self._root) 387 | elif _on == MachO.CsBlob.Expr.OpEnum.trusted_cert: 388 | self.data = MachO.CsBlob.Expr.CertSlotExpr(self._io, self, self._root) 389 | elif _on == MachO.CsBlob.Expr.OpEnum.and_op: 390 | self.data = MachO.CsBlob.Expr.AndExpr(self._io, self, self._root) 391 | elif _on == MachO.CsBlob.Expr.OpEnum.cert_generic: 392 | self.data = MachO.CsBlob.Expr.CertGenericExpr(self._io, self, self._root) 393 | elif _on == MachO.CsBlob.Expr.OpEnum.cert_field: 394 | self.data = MachO.CsBlob.Expr.CertFieldExpr(self._io, self, self._root) 395 | elif _on == MachO.CsBlob.Expr.OpEnum.cd_hash: 396 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 397 | elif _on == MachO.CsBlob.Expr.OpEnum.apple_generic_anchor: 398 | self.data = MachO.CsBlob.Expr.AppleGenericAnchorExpr(self._io, self, self._root) 399 | 400 | class InfoKeyFieldExpr(KaitaiStruct): 401 | def __init__(self, _io, _parent=None, _root=None): 402 | self._io = _io 403 | self._parent = _parent 404 | self._root = _root if _root else self 405 | self._read() 406 | 407 | def _read(self): 408 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 409 | self.match = MachO.CsBlob.Match(self._io, self, self._root) 410 | 411 | 412 | class CertSlotExpr(KaitaiStruct): 413 | def __init__(self, _io, _parent=None, _root=None): 414 | self._io = _io 415 | self._parent = _parent 416 | self._root = _root if _root else self 417 | self._read() 418 | 419 | def _read(self): 420 | self.value = KaitaiStream.resolve_enum(MachO.CsBlob.Expr.CertSlot, self._io.read_u4be()) 421 | 422 | 423 | class CertGenericExpr(KaitaiStruct): 424 | def __init__(self, _io, _parent=None, _root=None): 425 | self._io = _io 426 | self._parent = _parent 427 | self._root = _root if _root else self 428 | self._read() 429 | 430 | def _read(self): 431 | self.cert_slot = KaitaiStream.resolve_enum(MachO.CsBlob.Expr.CertSlot, self._io.read_u4be()) 432 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 433 | self.match = MachO.CsBlob.Match(self._io, self, self._root) 434 | 435 | 436 | class IdentExpr(KaitaiStruct): 437 | def __init__(self, _io, _parent=None, _root=None): 438 | self._io = _io 439 | self._parent = _parent 440 | self._root = _root if _root else self 441 | self._read() 442 | 443 | def _read(self): 444 | self.identifier = MachO.CsBlob.Data(self._io, self, self._root) 445 | 446 | 447 | class CertFieldExpr(KaitaiStruct): 448 | def __init__(self, _io, _parent=None, _root=None): 449 | self._io = _io 450 | self._parent = _parent 451 | self._root = _root if _root else self 452 | self._read() 453 | 454 | def _read(self): 455 | self.cert_slot = KaitaiStream.resolve_enum(MachO.CsBlob.Expr.CertSlot, self._io.read_u4be()) 456 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 457 | self.match = MachO.CsBlob.Match(self._io, self, self._root) 458 | 459 | 460 | class AnchorHashExpr(KaitaiStruct): 461 | def __init__(self, _io, _parent=None, _root=None): 462 | self._io = _io 463 | self._parent = _parent 464 | self._root = _root if _root else self 465 | self._read() 466 | 467 | def _read(self): 468 | self.cert_slot = KaitaiStream.resolve_enum(MachO.CsBlob.Expr.CertSlot, self._io.read_u4be()) 469 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 470 | 471 | 472 | class AppleGenericAnchorExpr(KaitaiStruct): 473 | def __init__(self, _io, _parent=None, _root=None): 474 | self._io = _io 475 | self._parent = _parent 476 | self._root = _root if _root else self 477 | self._read() 478 | 479 | def _read(self): 480 | pass 481 | 482 | @property 483 | def value(self): 484 | if hasattr(self, '_m_value'): 485 | return self._m_value 486 | 487 | self._m_value = u"anchor apple generic" 488 | return getattr(self, '_m_value', None) 489 | 490 | 491 | class EntitlementFieldExpr(KaitaiStruct): 492 | def __init__(self, _io, _parent=None, _root=None): 493 | self._io = _io 494 | self._parent = _parent 495 | self._root = _root if _root else self 496 | self._read() 497 | 498 | def _read(self): 499 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 500 | self.match = MachO.CsBlob.Match(self._io, self, self._root) 501 | 502 | 503 | class AndExpr(KaitaiStruct): 504 | def __init__(self, _io, _parent=None, _root=None): 505 | self._io = _io 506 | self._parent = _parent 507 | self._root = _root if _root else self 508 | self._read() 509 | 510 | def _read(self): 511 | self.left = MachO.CsBlob.Expr(self._io, self, self._root) 512 | self.right = MachO.CsBlob.Expr(self._io, self, self._root) 513 | 514 | 515 | class OrExpr(KaitaiStruct): 516 | def __init__(self, _io, _parent=None, _root=None): 517 | self._io = _io 518 | self._parent = _parent 519 | self._root = _root if _root else self 520 | self._read() 521 | 522 | def _read(self): 523 | self.left = MachO.CsBlob.Expr(self._io, self, self._root) 524 | self.right = MachO.CsBlob.Expr(self._io, self, self._root) 525 | 526 | 527 | 528 | class BlobIndex(KaitaiStruct): 529 | 530 | class CsslotType(Enum): 531 | code_directory = 0 532 | info_slot = 1 533 | requirements = 2 534 | resource_dir = 3 535 | application = 4 536 | entitlements = 5 537 | der_entitlements = 7 538 | alternate_code_directories = 4096 539 | signature_slot = 65536 540 | def __init__(self, _io, _parent=None, _root=None): 541 | self._io = _io 542 | self._parent = _parent 543 | self._root = _root if _root else self 544 | self._read() 545 | 546 | def _read(self): 547 | self.type = KaitaiStream.resolve_enum(MachO.CsBlob.BlobIndex.CsslotType, self._io.read_u4be()) 548 | self.offset = self._io.read_u4be() 549 | 550 | @property 551 | def blob(self): 552 | if hasattr(self, '_m_blob'): 553 | return self._m_blob 554 | 555 | io = self._parent._io 556 | _pos = io.pos() 557 | io.seek((self.offset - 8)) 558 | self._raw__m_blob = io.read_bytes_full() 559 | _io__raw__m_blob = KaitaiStream(BytesIO(self._raw__m_blob)) 560 | self._m_blob = MachO.CsBlob(_io__raw__m_blob, self, self._root) 561 | io.seek(_pos) 562 | return getattr(self, '_m_blob', None) 563 | 564 | 565 | class Match(KaitaiStruct): 566 | 567 | class Op(Enum): 568 | exists = 0 569 | equal = 1 570 | contains = 2 571 | begins_with = 3 572 | ends_with = 4 573 | less_than = 5 574 | greater_than = 6 575 | less_equal = 7 576 | greater_equal = 8 577 | def __init__(self, _io, _parent=None, _root=None): 578 | self._io = _io 579 | self._parent = _parent 580 | self._root = _root if _root else self 581 | self._read() 582 | 583 | def _read(self): 584 | self.match_op = KaitaiStream.resolve_enum(MachO.CsBlob.Match.Op, self._io.read_u4be()) 585 | if self.match_op != MachO.CsBlob.Match.Op.exists: 586 | self.data = MachO.CsBlob.Data(self._io, self, self._root) 587 | 588 | 589 | 590 | class Requirement(KaitaiStruct): 591 | def __init__(self, _io, _parent=None, _root=None): 592 | self._io = _io 593 | self._parent = _parent 594 | self._root = _root if _root else self 595 | self._read() 596 | 597 | def _read(self): 598 | self.kind = self._io.read_u4be() 599 | self.expr = MachO.CsBlob.Expr(self._io, self, self._root) 600 | 601 | 602 | class Requirements(KaitaiStruct): 603 | def __init__(self, _io, _parent=None, _root=None): 604 | self._io = _io 605 | self._parent = _parent 606 | self._root = _root if _root else self 607 | self._read() 608 | 609 | def _read(self): 610 | self.count = self._io.read_u4be() 611 | self.items = [] 612 | for i in range(self.count): 613 | self.items.append(MachO.CsBlob.RequirementsBlobIndex(self._io, self, self._root)) 614 | 615 | 616 | 617 | class BlobWrapper(KaitaiStruct): 618 | def __init__(self, _io, _parent=None, _root=None): 619 | self._io = _io 620 | self._parent = _parent 621 | self._root = _root if _root else self 622 | self._read() 623 | 624 | def _read(self): 625 | self.data = self._io.read_bytes_full() 626 | 627 | 628 | class Entitlements(KaitaiStruct): 629 | def __init__(self, _io, _parent=None, _root=None): 630 | self._io = _io 631 | self._parent = _parent 632 | self._root = _root if _root else self 633 | self._read() 634 | 635 | def _read(self): 636 | self.data = self._io.read_bytes_full() 637 | 638 | 639 | class RequirementsBlobIndex(KaitaiStruct): 640 | 641 | class RequirementType(Enum): 642 | host = 1 643 | guest = 2 644 | designated = 3 645 | library = 4 646 | def __init__(self, _io, _parent=None, _root=None): 647 | self._io = _io 648 | self._parent = _parent 649 | self._root = _root if _root else self 650 | self._read() 651 | 652 | def _read(self): 653 | self.type = KaitaiStream.resolve_enum(MachO.CsBlob.RequirementsBlobIndex.RequirementType, self._io.read_u4be()) 654 | self.offset = self._io.read_u4be() 655 | 656 | @property 657 | def value(self): 658 | if hasattr(self, '_m_value'): 659 | return self._m_value 660 | 661 | _pos = self._io.pos() 662 | self._io.seek((self.offset - 8)) 663 | self._m_value = MachO.CsBlob(self._io, self, self._root) 664 | self._io.seek(_pos) 665 | return getattr(self, '_m_value', None) 666 | 667 | 668 | 669 | class BuildVersionCommand(KaitaiStruct): 670 | def __init__(self, _io, _parent=None, _root=None): 671 | self._io = _io 672 | self._parent = _parent 673 | self._root = _root if _root else self 674 | self._read() 675 | 676 | def _read(self): 677 | self.platform = self._io.read_u4le() 678 | self.minos = self._io.read_u4le() 679 | self.sdk = self._io.read_u4le() 680 | self.ntools = self._io.read_u4le() 681 | self.tools = [] 682 | for i in range(self.ntools): 683 | self.tools.append(MachO.BuildVersionCommand.BuildToolVersion(self._io, self, self._root)) 684 | 685 | 686 | class BuildToolVersion(KaitaiStruct): 687 | def __init__(self, _io, _parent=None, _root=None): 688 | self._io = _io 689 | self._parent = _parent 690 | self._root = _root if _root else self 691 | self._read() 692 | 693 | def _read(self): 694 | self.tool = self._io.read_u4le() 695 | self.version = self._io.read_u4le() 696 | 697 | 698 | 699 | class RoutinesCommand(KaitaiStruct): 700 | def __init__(self, _io, _parent=None, _root=None): 701 | self._io = _io 702 | self._parent = _parent 703 | self._root = _root if _root else self 704 | self._read() 705 | 706 | def _read(self): 707 | self.init_address = self._io.read_u4le() 708 | self.init_module = self._io.read_u4le() 709 | self.reserved = self._io.read_bytes(24) 710 | 711 | 712 | class MachoFlags(KaitaiStruct): 713 | def __init__(self, value, _io, _parent=None, _root=None): 714 | self._io = _io 715 | self._parent = _parent 716 | self._root = _root if _root else self 717 | self.value = value 718 | self._read() 719 | 720 | def _read(self): 721 | pass 722 | 723 | @property 724 | def subsections_via_symbols(self): 725 | """safe to divide up the sections into sub-sections via symbols for dead code stripping.""" 726 | if hasattr(self, '_m_subsections_via_symbols'): 727 | return self._m_subsections_via_symbols 728 | 729 | self._m_subsections_via_symbols = (self.value & 8192) != 0 730 | return getattr(self, '_m_subsections_via_symbols', None) 731 | 732 | @property 733 | def dead_strippable_dylib(self): 734 | if hasattr(self, '_m_dead_strippable_dylib'): 735 | return self._m_dead_strippable_dylib 736 | 737 | self._m_dead_strippable_dylib = (self.value & 4194304) != 0 738 | return getattr(self, '_m_dead_strippable_dylib', None) 739 | 740 | @property 741 | def weak_defines(self): 742 | """the final linked image contains external weak symbols.""" 743 | if hasattr(self, '_m_weak_defines'): 744 | return self._m_weak_defines 745 | 746 | self._m_weak_defines = (self.value & 32768) != 0 747 | return getattr(self, '_m_weak_defines', None) 748 | 749 | @property 750 | def prebound(self): 751 | """the file has its dynamic undefined references prebound.""" 752 | if hasattr(self, '_m_prebound'): 753 | return self._m_prebound 754 | 755 | self._m_prebound = (self.value & 16) != 0 756 | return getattr(self, '_m_prebound', None) 757 | 758 | @property 759 | def all_mods_bound(self): 760 | """indicates that this binary binds to all two-level namespace modules of its dependent libraries. only used when MH_PREBINDABLE and MH_TWOLEVEL are both set.""" 761 | if hasattr(self, '_m_all_mods_bound'): 762 | return self._m_all_mods_bound 763 | 764 | self._m_all_mods_bound = (self.value & 4096) != 0 765 | return getattr(self, '_m_all_mods_bound', None) 766 | 767 | @property 768 | def has_tlv_descriptors(self): 769 | if hasattr(self, '_m_has_tlv_descriptors'): 770 | return self._m_has_tlv_descriptors 771 | 772 | self._m_has_tlv_descriptors = (self.value & 8388608) != 0 773 | return getattr(self, '_m_has_tlv_descriptors', None) 774 | 775 | @property 776 | def force_flat(self): 777 | """the executable is forcing all images to use flat name space bindings.""" 778 | if hasattr(self, '_m_force_flat'): 779 | return self._m_force_flat 780 | 781 | self._m_force_flat = (self.value & 256) != 0 782 | return getattr(self, '_m_force_flat', None) 783 | 784 | @property 785 | def root_safe(self): 786 | """When this bit is set, the binary declares it is safe for use in processes with uid zero.""" 787 | if hasattr(self, '_m_root_safe'): 788 | return self._m_root_safe 789 | 790 | self._m_root_safe = (self.value & 262144) != 0 791 | return getattr(self, '_m_root_safe', None) 792 | 793 | @property 794 | def no_undefs(self): 795 | """the object file has no undefined references.""" 796 | if hasattr(self, '_m_no_undefs'): 797 | return self._m_no_undefs 798 | 799 | self._m_no_undefs = (self.value & 1) != 0 800 | return getattr(self, '_m_no_undefs', None) 801 | 802 | @property 803 | def setuid_safe(self): 804 | """When this bit is set, the binary declares it is safe for use in processes when issetugid() is true.""" 805 | if hasattr(self, '_m_setuid_safe'): 806 | return self._m_setuid_safe 807 | 808 | self._m_setuid_safe = (self.value & 524288) != 0 809 | return getattr(self, '_m_setuid_safe', None) 810 | 811 | @property 812 | def no_heap_execution(self): 813 | if hasattr(self, '_m_no_heap_execution'): 814 | return self._m_no_heap_execution 815 | 816 | self._m_no_heap_execution = (self.value & 16777216) != 0 817 | return getattr(self, '_m_no_heap_execution', None) 818 | 819 | @property 820 | def no_reexported_dylibs(self): 821 | """When this bit is set on a dylib, the static linker does not need to examine dependent dylibs to see if any are re-exported.""" 822 | if hasattr(self, '_m_no_reexported_dylibs'): 823 | return self._m_no_reexported_dylibs 824 | 825 | self._m_no_reexported_dylibs = (self.value & 1048576) != 0 826 | return getattr(self, '_m_no_reexported_dylibs', None) 827 | 828 | @property 829 | def no_multi_defs(self): 830 | """this umbrella guarantees no multiple defintions of symbols in its sub-images so the two-level namespace hints can always be used.""" 831 | if hasattr(self, '_m_no_multi_defs'): 832 | return self._m_no_multi_defs 833 | 834 | self._m_no_multi_defs = (self.value & 512) != 0 835 | return getattr(self, '_m_no_multi_defs', None) 836 | 837 | @property 838 | def app_extension_safe(self): 839 | if hasattr(self, '_m_app_extension_safe'): 840 | return self._m_app_extension_safe 841 | 842 | self._m_app_extension_safe = (self.value & 33554432) != 0 843 | return getattr(self, '_m_app_extension_safe', None) 844 | 845 | @property 846 | def prebindable(self): 847 | """the binary is not prebound but can have its prebinding redone. only used when MH_PREBOUND is not set.""" 848 | if hasattr(self, '_m_prebindable'): 849 | return self._m_prebindable 850 | 851 | self._m_prebindable = (self.value & 2048) != 0 852 | return getattr(self, '_m_prebindable', None) 853 | 854 | @property 855 | def incr_link(self): 856 | """the object file is the output of an incremental link against a base file and can't be link edited again.""" 857 | if hasattr(self, '_m_incr_link'): 858 | return self._m_incr_link 859 | 860 | self._m_incr_link = (self.value & 2) != 0 861 | return getattr(self, '_m_incr_link', None) 862 | 863 | @property 864 | def bind_at_load(self): 865 | """the object file's undefined references are bound by the dynamic linker when loaded.""" 866 | if hasattr(self, '_m_bind_at_load'): 867 | return self._m_bind_at_load 868 | 869 | self._m_bind_at_load = (self.value & 8) != 0 870 | return getattr(self, '_m_bind_at_load', None) 871 | 872 | @property 873 | def canonical(self): 874 | """the binary has been canonicalized via the unprebind operation.""" 875 | if hasattr(self, '_m_canonical'): 876 | return self._m_canonical 877 | 878 | self._m_canonical = (self.value & 16384) != 0 879 | return getattr(self, '_m_canonical', None) 880 | 881 | @property 882 | def two_level(self): 883 | """the image is using two-level name space bindings.""" 884 | if hasattr(self, '_m_two_level'): 885 | return self._m_two_level 886 | 887 | self._m_two_level = (self.value & 128) != 0 888 | return getattr(self, '_m_two_level', None) 889 | 890 | @property 891 | def split_segs(self): 892 | """the file has its read-only and read-write segments split.""" 893 | if hasattr(self, '_m_split_segs'): 894 | return self._m_split_segs 895 | 896 | self._m_split_segs = (self.value & 32) != 0 897 | return getattr(self, '_m_split_segs', None) 898 | 899 | @property 900 | def lazy_init(self): 901 | """the shared library init routine is to be run lazily via catching memory faults to its writeable segments (obsolete).""" 902 | if hasattr(self, '_m_lazy_init'): 903 | return self._m_lazy_init 904 | 905 | self._m_lazy_init = (self.value & 64) != 0 906 | return getattr(self, '_m_lazy_init', None) 907 | 908 | @property 909 | def allow_stack_execution(self): 910 | """When this bit is set, all stacks in the task will be given stack execution privilege. Only used in MH_EXECUTE filetypes.""" 911 | if hasattr(self, '_m_allow_stack_execution'): 912 | return self._m_allow_stack_execution 913 | 914 | self._m_allow_stack_execution = (self.value & 131072) != 0 915 | return getattr(self, '_m_allow_stack_execution', None) 916 | 917 | @property 918 | def binds_to_weak(self): 919 | """the final linked image uses weak symbols.""" 920 | if hasattr(self, '_m_binds_to_weak'): 921 | return self._m_binds_to_weak 922 | 923 | self._m_binds_to_weak = (self.value & 65536) != 0 924 | return getattr(self, '_m_binds_to_weak', None) 925 | 926 | @property 927 | def no_fix_prebinding(self): 928 | """do not have dyld notify the prebinding agent about this executable.""" 929 | if hasattr(self, '_m_no_fix_prebinding'): 930 | return self._m_no_fix_prebinding 931 | 932 | self._m_no_fix_prebinding = (self.value & 1024) != 0 933 | return getattr(self, '_m_no_fix_prebinding', None) 934 | 935 | @property 936 | def dyld_link(self): 937 | """the object file is input for the dynamic linker and can't be staticly link edited again.""" 938 | if hasattr(self, '_m_dyld_link'): 939 | return self._m_dyld_link 940 | 941 | self._m_dyld_link = (self.value & 4) != 0 942 | return getattr(self, '_m_dyld_link', None) 943 | 944 | @property 945 | def pie(self): 946 | """When this bit is set, the OS will load the main executable at a random address. Only used in MH_EXECUTE filetypes.""" 947 | if hasattr(self, '_m_pie'): 948 | return self._m_pie 949 | 950 | self._m_pie = (self.value & 2097152) != 0 951 | return getattr(self, '_m_pie', None) 952 | 953 | 954 | class RoutinesCommand64(KaitaiStruct): 955 | def __init__(self, _io, _parent=None, _root=None): 956 | self._io = _io 957 | self._parent = _parent 958 | self._root = _root if _root else self 959 | self._read() 960 | 961 | def _read(self): 962 | self.init_address = self._io.read_u8le() 963 | self.init_module = self._io.read_u8le() 964 | self.reserved = self._io.read_bytes(48) 965 | 966 | 967 | class LinkerOptionCommand(KaitaiStruct): 968 | def __init__(self, _io, _parent=None, _root=None): 969 | self._io = _io 970 | self._parent = _parent 971 | self._root = _root if _root else self 972 | self._read() 973 | 974 | def _read(self): 975 | self.num_strings = self._io.read_u4le() 976 | self.strings = [] 977 | for i in range(self.num_strings): 978 | self.strings.append((self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8")) 979 | 980 | 981 | 982 | class SegmentCommand64(KaitaiStruct): 983 | def __init__(self, _io, _parent=None, _root=None): 984 | self._io = _io 985 | self._parent = _parent 986 | self._root = _root if _root else self 987 | self._read() 988 | 989 | def _read(self): 990 | self.segname = (KaitaiStream.bytes_strip_right(self._io.read_bytes(16), 0)).decode(u"ascii") 991 | self.vmaddr = self._io.read_u8le() 992 | self.vmsize = self._io.read_u8le() 993 | self.fileoff = self._io.read_u8le() 994 | self.filesize = self._io.read_u8le() 995 | self.maxprot = MachO.VmProt(self._io, self, self._root) 996 | self.initprot = MachO.VmProt(self._io, self, self._root) 997 | self.nsects = self._io.read_u4le() 998 | self.flags = self._io.read_u4le() 999 | self.sections = [] 1000 | for i in range(self.nsects): 1001 | self.sections.append(MachO.SegmentCommand64.Section64(self._io, self, self._root)) 1002 | 1003 | 1004 | class Section64(KaitaiStruct): 1005 | def __init__(self, _io, _parent=None, _root=None): 1006 | self._io = _io 1007 | self._parent = _parent 1008 | self._root = _root if _root else self 1009 | self._read() 1010 | 1011 | def _read(self): 1012 | self.sect_name = (KaitaiStream.bytes_strip_right(self._io.read_bytes(16), 0)).decode(u"ascii") 1013 | self.seg_name = (KaitaiStream.bytes_strip_right(self._io.read_bytes(16), 0)).decode(u"ascii") 1014 | self.addr = self._io.read_u8le() 1015 | self.size = self._io.read_u8le() 1016 | self.offset = self._io.read_u4le() 1017 | self.align = self._io.read_u4le() 1018 | self.reloff = self._io.read_u4le() 1019 | self.nreloc = self._io.read_u4le() 1020 | self.flags = self._io.read_u4le() 1021 | self.reserved1 = self._io.read_u4le() 1022 | self.reserved2 = self._io.read_u4le() 1023 | self.reserved3 = self._io.read_u4le() 1024 | 1025 | class CfStringList(KaitaiStruct): 1026 | def __init__(self, _io, _parent=None, _root=None): 1027 | self._io = _io 1028 | self._parent = _parent 1029 | self._root = _root if _root else self 1030 | self._read() 1031 | 1032 | def _read(self): 1033 | self.items = [] 1034 | i = 0 1035 | while not self._io.is_eof(): 1036 | self.items.append(MachO.SegmentCommand64.Section64.CfString(self._io, self, self._root)) 1037 | i += 1 1038 | 1039 | 1040 | 1041 | class CfString(KaitaiStruct): 1042 | def __init__(self, _io, _parent=None, _root=None): 1043 | self._io = _io 1044 | self._parent = _parent 1045 | self._root = _root if _root else self 1046 | self._read() 1047 | 1048 | def _read(self): 1049 | self.isa = self._io.read_u8le() 1050 | self.info = self._io.read_u8le() 1051 | self.data = self._io.read_u8le() 1052 | self.length = self._io.read_u8le() 1053 | 1054 | 1055 | class EhFrameItem(KaitaiStruct): 1056 | def __init__(self, _io, _parent=None, _root=None): 1057 | self._io = _io 1058 | self._parent = _parent 1059 | self._root = _root if _root else self 1060 | self._read() 1061 | 1062 | def _read(self): 1063 | self.length = self._io.read_u4le() 1064 | if self.length == 4294967295: 1065 | self.length64 = self._io.read_u8le() 1066 | 1067 | self.id = self._io.read_u4le() 1068 | if self.length > 0: 1069 | _on = self.id 1070 | if _on == 0: 1071 | self._raw_body = self._io.read_bytes((self.length - 4)) 1072 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1073 | self.body = MachO.SegmentCommand64.Section64.EhFrameItem.Cie(_io__raw_body, self, self._root) 1074 | else: 1075 | self.body = self._io.read_bytes((self.length - 4)) 1076 | 1077 | 1078 | class CharChain(KaitaiStruct): 1079 | def __init__(self, _io, _parent=None, _root=None): 1080 | self._io = _io 1081 | self._parent = _parent 1082 | self._root = _root if _root else self 1083 | self._read() 1084 | 1085 | def _read(self): 1086 | self.chr = self._io.read_u1() 1087 | if self.chr != 0: 1088 | self.next = MachO.SegmentCommand64.Section64.EhFrameItem.CharChain(self._io, self, self._root) 1089 | 1090 | 1091 | 1092 | class Cie(KaitaiStruct): 1093 | def __init__(self, _io, _parent=None, _root=None): 1094 | self._io = _io 1095 | self._parent = _parent 1096 | self._root = _root if _root else self 1097 | self._read() 1098 | 1099 | def _read(self): 1100 | self.version = self._io.read_u1() 1101 | self.aug_str = MachO.SegmentCommand64.Section64.EhFrameItem.CharChain(self._io, self, self._root) 1102 | self.code_alignment_factor = MachO.Uleb128(self._io, self, self._root) 1103 | self.data_alignment_factor = MachO.Uleb128(self._io, self, self._root) 1104 | self.return_address_register = self._io.read_u1() 1105 | if self.aug_str.chr == 122: 1106 | self.augmentation = MachO.SegmentCommand64.Section64.EhFrameItem.AugmentationEntry(self._io, self, self._root) 1107 | 1108 | 1109 | 1110 | class AugmentationEntry(KaitaiStruct): 1111 | def __init__(self, _io, _parent=None, _root=None): 1112 | self._io = _io 1113 | self._parent = _parent 1114 | self._root = _root if _root else self 1115 | self._read() 1116 | 1117 | def _read(self): 1118 | self.length = MachO.Uleb128(self._io, self, self._root) 1119 | if self._parent.aug_str.next.chr == 82: 1120 | self.fde_pointer_encoding = self._io.read_u1() 1121 | 1122 | 1123 | 1124 | 1125 | class EhFrame(KaitaiStruct): 1126 | def __init__(self, _io, _parent=None, _root=None): 1127 | self._io = _io 1128 | self._parent = _parent 1129 | self._root = _root if _root else self 1130 | self._read() 1131 | 1132 | def _read(self): 1133 | self.items = [] 1134 | i = 0 1135 | while not self._io.is_eof(): 1136 | self.items.append(MachO.SegmentCommand64.Section64.EhFrameItem(self._io, self, self._root)) 1137 | i += 1 1138 | 1139 | 1140 | 1141 | class PointerList(KaitaiStruct): 1142 | def __init__(self, _io, _parent=None, _root=None): 1143 | self._io = _io 1144 | self._parent = _parent 1145 | self._root = _root if _root else self 1146 | self._read() 1147 | 1148 | def _read(self): 1149 | self.items = [] 1150 | i = 0 1151 | while not self._io.is_eof(): 1152 | self.items.append(self._io.read_u8le()) 1153 | i += 1 1154 | 1155 | 1156 | 1157 | class StringList(KaitaiStruct): 1158 | def __init__(self, _io, _parent=None, _root=None): 1159 | self._io = _io 1160 | self._parent = _parent 1161 | self._root = _root if _root else self 1162 | self._read() 1163 | 1164 | def _read(self): 1165 | self.strings = [] 1166 | i = 0 1167 | while not self._io.is_eof(): 1168 | self.strings.append((self._io.read_bytes_term(0, False, True, True)).decode(u"ascii")) 1169 | i += 1 1170 | 1171 | 1172 | 1173 | @property 1174 | def data(self): 1175 | if hasattr(self, '_m_data'): 1176 | return self._m_data 1177 | 1178 | io = self._root._io 1179 | _pos = io.pos() 1180 | io.seek(self.offset) 1181 | _on = self.sect_name 1182 | if _on == u"__objc_nlclslist": 1183 | self._raw__m_data = io.read_bytes(self.size) 1184 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1185 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1186 | elif _on == u"__objc_methname": 1187 | self._raw__m_data = io.read_bytes(self.size) 1188 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1189 | self._m_data = MachO.SegmentCommand64.Section64.StringList(_io__raw__m_data, self, self._root) 1190 | elif _on == u"__nl_symbol_ptr": 1191 | self._raw__m_data = io.read_bytes(self.size) 1192 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1193 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1194 | elif _on == u"__la_symbol_ptr": 1195 | self._raw__m_data = io.read_bytes(self.size) 1196 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1197 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1198 | elif _on == u"__objc_selrefs": 1199 | self._raw__m_data = io.read_bytes(self.size) 1200 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1201 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1202 | elif _on == u"__cstring": 1203 | self._raw__m_data = io.read_bytes(self.size) 1204 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1205 | self._m_data = MachO.SegmentCommand64.Section64.StringList(_io__raw__m_data, self, self._root) 1206 | elif _on == u"__objc_classlist": 1207 | self._raw__m_data = io.read_bytes(self.size) 1208 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1209 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1210 | elif _on == u"__objc_protolist": 1211 | self._raw__m_data = io.read_bytes(self.size) 1212 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1213 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1214 | elif _on == u"__objc_imageinfo": 1215 | self._raw__m_data = io.read_bytes(self.size) 1216 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1217 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1218 | elif _on == u"__objc_methtype": 1219 | self._raw__m_data = io.read_bytes(self.size) 1220 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1221 | self._m_data = MachO.SegmentCommand64.Section64.StringList(_io__raw__m_data, self, self._root) 1222 | elif _on == u"__cfstring": 1223 | self._raw__m_data = io.read_bytes(self.size) 1224 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1225 | self._m_data = MachO.SegmentCommand64.Section64.CfStringList(_io__raw__m_data, self, self._root) 1226 | elif _on == u"__objc_classrefs": 1227 | self._raw__m_data = io.read_bytes(self.size) 1228 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1229 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1230 | elif _on == u"__objc_protorefs": 1231 | self._raw__m_data = io.read_bytes(self.size) 1232 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1233 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1234 | elif _on == u"__objc_classname": 1235 | self._raw__m_data = io.read_bytes(self.size) 1236 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1237 | self._m_data = MachO.SegmentCommand64.Section64.StringList(_io__raw__m_data, self, self._root) 1238 | elif _on == u"__got": 1239 | self._raw__m_data = io.read_bytes(self.size) 1240 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1241 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1242 | elif _on == u"__eh_frame": 1243 | self._raw__m_data = io.read_bytes(self.size) 1244 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1245 | self._m_data = MachO.SegmentCommand64.Section64.EhFrame(_io__raw__m_data, self, self._root) 1246 | elif _on == u"__objc_superrefs": 1247 | self._raw__m_data = io.read_bytes(self.size) 1248 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 1249 | self._m_data = MachO.SegmentCommand64.Section64.PointerList(_io__raw__m_data, self, self._root) 1250 | else: 1251 | self._m_data = io.read_bytes(self.size) 1252 | io.seek(_pos) 1253 | return getattr(self, '_m_data', None) 1254 | 1255 | 1256 | 1257 | class VmProt(KaitaiStruct): 1258 | def __init__(self, _io, _parent=None, _root=None): 1259 | self._io = _io 1260 | self._parent = _parent 1261 | self._root = _root if _root else self 1262 | self._read() 1263 | 1264 | def _read(self): 1265 | self.strip_read = self._io.read_bits_int_be(1) != 0 1266 | self.is_mask = self._io.read_bits_int_be(1) != 0 1267 | self.reserved0 = self._io.read_bits_int_be(1) != 0 1268 | self.copy = self._io.read_bits_int_be(1) != 0 1269 | self.no_change = self._io.read_bits_int_be(1) != 0 1270 | self.execute = self._io.read_bits_int_be(1) != 0 1271 | self.write = self._io.read_bits_int_be(1) != 0 1272 | self.read = self._io.read_bits_int_be(1) != 0 1273 | self.reserved1 = self._io.read_bits_int_be(24) 1274 | 1275 | 1276 | class DysymtabCommand(KaitaiStruct): 1277 | def __init__(self, _io, _parent=None, _root=None): 1278 | self._io = _io 1279 | self._parent = _parent 1280 | self._root = _root if _root else self 1281 | self._read() 1282 | 1283 | def _read(self): 1284 | self.i_local_sym = self._io.read_u4le() 1285 | self.n_local_sym = self._io.read_u4le() 1286 | self.i_ext_def_sym = self._io.read_u4le() 1287 | self.n_ext_def_sym = self._io.read_u4le() 1288 | self.i_undef_sym = self._io.read_u4le() 1289 | self.n_undef_sym = self._io.read_u4le() 1290 | self.toc_off = self._io.read_u4le() 1291 | self.n_toc = self._io.read_u4le() 1292 | self.mod_tab_off = self._io.read_u4le() 1293 | self.n_mod_tab = self._io.read_u4le() 1294 | self.ext_ref_sym_off = self._io.read_u4le() 1295 | self.n_ext_ref_syms = self._io.read_u4le() 1296 | self.indirect_sym_off = self._io.read_u4le() 1297 | self.n_indirect_syms = self._io.read_u4le() 1298 | self.ext_rel_off = self._io.read_u4le() 1299 | self.n_ext_rel = self._io.read_u4le() 1300 | self.loc_rel_off = self._io.read_u4le() 1301 | self.n_loc_rel = self._io.read_u4le() 1302 | 1303 | @property 1304 | def indirect_symbols(self): 1305 | if hasattr(self, '_m_indirect_symbols'): 1306 | return self._m_indirect_symbols 1307 | 1308 | io = self._root._io 1309 | _pos = io.pos() 1310 | io.seek(self.indirect_sym_off) 1311 | self._m_indirect_symbols = [] 1312 | for i in range(self.n_indirect_syms): 1313 | self._m_indirect_symbols.append(io.read_u4le()) 1314 | 1315 | io.seek(_pos) 1316 | return getattr(self, '_m_indirect_symbols', None) 1317 | 1318 | 1319 | class MachHeader(KaitaiStruct): 1320 | def __init__(self, _io, _parent=None, _root=None): 1321 | self._io = _io 1322 | self._parent = _parent 1323 | self._root = _root if _root else self 1324 | self._read() 1325 | 1326 | def _read(self): 1327 | self.cputype = KaitaiStream.resolve_enum(MachO.CpuType, self._io.read_u4le()) 1328 | self.cpusubtype = self._io.read_u4le() 1329 | self.filetype = KaitaiStream.resolve_enum(MachO.FileType, self._io.read_u4le()) 1330 | self.ncmds = self._io.read_u4le() 1331 | self.sizeofcmds = self._io.read_u4le() 1332 | self.flags = self._io.read_u4le() 1333 | if ((self._root.magic == MachO.MagicType.macho_be_x64) or (self._root.magic == MachO.MagicType.macho_le_x64)) : 1334 | self.reserved = self._io.read_u4le() 1335 | 1336 | 1337 | @property 1338 | def flags_obj(self): 1339 | if hasattr(self, '_m_flags_obj'): 1340 | return self._m_flags_obj 1341 | 1342 | self._m_flags_obj = MachO.MachoFlags(self.flags, self._io, self, self._root) 1343 | return getattr(self, '_m_flags_obj', None) 1344 | 1345 | 1346 | class LinkeditDataCommand(KaitaiStruct): 1347 | def __init__(self, _io, _parent=None, _root=None): 1348 | self._io = _io 1349 | self._parent = _parent 1350 | self._root = _root if _root else self 1351 | self._read() 1352 | 1353 | def _read(self): 1354 | self.data_off = self._io.read_u4le() 1355 | self.data_size = self._io.read_u4le() 1356 | 1357 | 1358 | class SubCommand(KaitaiStruct): 1359 | def __init__(self, _io, _parent=None, _root=None): 1360 | self._io = _io 1361 | self._parent = _parent 1362 | self._root = _root if _root else self 1363 | self._read() 1364 | 1365 | def _read(self): 1366 | self.name = MachO.LcStr(self._io, self, self._root) 1367 | 1368 | 1369 | class TwolevelHintsCommand(KaitaiStruct): 1370 | def __init__(self, _io, _parent=None, _root=None): 1371 | self._io = _io 1372 | self._parent = _parent 1373 | self._root = _root if _root else self 1374 | self._read() 1375 | 1376 | def _read(self): 1377 | self.offset = self._io.read_u4le() 1378 | self.num_hints = self._io.read_u4le() 1379 | 1380 | 1381 | class Version(KaitaiStruct): 1382 | def __init__(self, _io, _parent=None, _root=None): 1383 | self._io = _io 1384 | self._parent = _parent 1385 | self._root = _root if _root else self 1386 | self._read() 1387 | 1388 | def _read(self): 1389 | self.p1 = self._io.read_u1() 1390 | self.minor = self._io.read_u1() 1391 | self.major = self._io.read_u1() 1392 | self.release = self._io.read_u1() 1393 | 1394 | 1395 | class EncryptionInfoCommand(KaitaiStruct): 1396 | def __init__(self, _io, _parent=None, _root=None): 1397 | self._io = _io 1398 | self._parent = _parent 1399 | self._root = _root if _root else self 1400 | self._read() 1401 | 1402 | def _read(self): 1403 | self.cryptoff = self._io.read_u4le() 1404 | self.cryptsize = self._io.read_u4le() 1405 | self.cryptid = self._io.read_u4le() 1406 | if ((self._root.magic == MachO.MagicType.macho_be_x64) or (self._root.magic == MachO.MagicType.macho_le_x64)) : 1407 | self.pad = self._io.read_u4le() 1408 | 1409 | 1410 | 1411 | class CodeSignatureCommand(KaitaiStruct): 1412 | def __init__(self, _io, _parent=None, _root=None): 1413 | self._io = _io 1414 | self._parent = _parent 1415 | self._root = _root if _root else self 1416 | self._read() 1417 | 1418 | def _read(self): 1419 | self.data_off = self._io.read_u4le() 1420 | self.data_size = self._io.read_u4le() 1421 | 1422 | @property 1423 | def code_signature(self): 1424 | if hasattr(self, '_m_code_signature'): 1425 | return self._m_code_signature 1426 | 1427 | io = self._root._io 1428 | _pos = io.pos() 1429 | io.seek(self.data_off) 1430 | self._raw__m_code_signature = io.read_bytes(self.data_size) 1431 | _io__raw__m_code_signature = KaitaiStream(BytesIO(self._raw__m_code_signature)) 1432 | self._m_code_signature = MachO.CsBlob(_io__raw__m_code_signature, self, self._root) 1433 | io.seek(_pos) 1434 | return getattr(self, '_m_code_signature', None) 1435 | 1436 | 1437 | class DyldInfoCommand(KaitaiStruct): 1438 | 1439 | class BindOpcode(Enum): 1440 | done = 0 1441 | set_dylib_ordinal_immediate = 16 1442 | set_dylib_ordinal_uleb = 32 1443 | set_dylib_special_immediate = 48 1444 | set_symbol_trailing_flags_immediate = 64 1445 | set_type_immediate = 80 1446 | set_append_sleb = 96 1447 | set_segment_and_offset_uleb = 112 1448 | add_address_uleb = 128 1449 | do_bind = 144 1450 | do_bind_add_address_uleb = 160 1451 | do_bind_add_address_immediate_scaled = 176 1452 | do_bind_uleb_times_skipping_uleb = 192 1453 | def __init__(self, _io, _parent=None, _root=None): 1454 | self._io = _io 1455 | self._parent = _parent 1456 | self._root = _root if _root else self 1457 | self._read() 1458 | 1459 | def _read(self): 1460 | self.rebase_off = self._io.read_u4le() 1461 | self.rebase_size = self._io.read_u4le() 1462 | self.bind_off = self._io.read_u4le() 1463 | self.bind_size = self._io.read_u4le() 1464 | self.weak_bind_off = self._io.read_u4le() 1465 | self.weak_bind_size = self._io.read_u4le() 1466 | self.lazy_bind_off = self._io.read_u4le() 1467 | self.lazy_bind_size = self._io.read_u4le() 1468 | self.export_off = self._io.read_u4le() 1469 | self.export_size = self._io.read_u4le() 1470 | 1471 | class BindItem(KaitaiStruct): 1472 | def __init__(self, _io, _parent=None, _root=None): 1473 | self._io = _io 1474 | self._parent = _parent 1475 | self._root = _root if _root else self 1476 | self._read() 1477 | 1478 | def _read(self): 1479 | self.opcode_and_immediate = self._io.read_u1() 1480 | if ((self.opcode == MachO.DyldInfoCommand.BindOpcode.set_dylib_ordinal_uleb) or (self.opcode == MachO.DyldInfoCommand.BindOpcode.set_append_sleb) or (self.opcode == MachO.DyldInfoCommand.BindOpcode.set_segment_and_offset_uleb) or (self.opcode == MachO.DyldInfoCommand.BindOpcode.add_address_uleb) or (self.opcode == MachO.DyldInfoCommand.BindOpcode.do_bind_add_address_uleb) or (self.opcode == MachO.DyldInfoCommand.BindOpcode.do_bind_uleb_times_skipping_uleb)) : 1481 | self.uleb = MachO.Uleb128(self._io, self, self._root) 1482 | 1483 | if self.opcode == MachO.DyldInfoCommand.BindOpcode.do_bind_uleb_times_skipping_uleb: 1484 | self.skip = MachO.Uleb128(self._io, self, self._root) 1485 | 1486 | if self.opcode == MachO.DyldInfoCommand.BindOpcode.set_symbol_trailing_flags_immediate: 1487 | self.symbol = (self._io.read_bytes_term(0, False, True, True)).decode(u"ascii") 1488 | 1489 | 1490 | @property 1491 | def opcode(self): 1492 | if hasattr(self, '_m_opcode'): 1493 | return self._m_opcode 1494 | 1495 | self._m_opcode = KaitaiStream.resolve_enum(MachO.DyldInfoCommand.BindOpcode, (self.opcode_and_immediate & 240)) 1496 | return getattr(self, '_m_opcode', None) 1497 | 1498 | @property 1499 | def immediate(self): 1500 | if hasattr(self, '_m_immediate'): 1501 | return self._m_immediate 1502 | 1503 | self._m_immediate = (self.opcode_and_immediate & 15) 1504 | return getattr(self, '_m_immediate', None) 1505 | 1506 | 1507 | class RebaseData(KaitaiStruct): 1508 | 1509 | class Opcode(Enum): 1510 | done = 0 1511 | set_type_immediate = 16 1512 | set_segment_and_offset_uleb = 32 1513 | add_address_uleb = 48 1514 | add_address_immediate_scaled = 64 1515 | do_rebase_immediate_times = 80 1516 | do_rebase_uleb_times = 96 1517 | do_rebase_add_address_uleb = 112 1518 | do_rebase_uleb_times_skipping_uleb = 128 1519 | def __init__(self, _io, _parent=None, _root=None): 1520 | self._io = _io 1521 | self._parent = _parent 1522 | self._root = _root if _root else self 1523 | self._read() 1524 | 1525 | def _read(self): 1526 | self.items = [] 1527 | i = 0 1528 | while True: 1529 | _ = MachO.DyldInfoCommand.RebaseData.RebaseItem(self._io, self, self._root) 1530 | self.items.append(_) 1531 | if _.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.done: 1532 | break 1533 | i += 1 1534 | 1535 | class RebaseItem(KaitaiStruct): 1536 | def __init__(self, _io, _parent=None, _root=None): 1537 | self._io = _io 1538 | self._parent = _parent 1539 | self._root = _root if _root else self 1540 | self._read() 1541 | 1542 | def _read(self): 1543 | self.opcode_and_immediate = self._io.read_u1() 1544 | if ((self.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.set_segment_and_offset_uleb) or (self.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.add_address_uleb) or (self.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.do_rebase_uleb_times) or (self.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.do_rebase_add_address_uleb) or (self.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.do_rebase_uleb_times_skipping_uleb)) : 1545 | self.uleb = MachO.Uleb128(self._io, self, self._root) 1546 | 1547 | if self.opcode == MachO.DyldInfoCommand.RebaseData.Opcode.do_rebase_uleb_times_skipping_uleb: 1548 | self.skip = MachO.Uleb128(self._io, self, self._root) 1549 | 1550 | 1551 | @property 1552 | def opcode(self): 1553 | if hasattr(self, '_m_opcode'): 1554 | return self._m_opcode 1555 | 1556 | self._m_opcode = KaitaiStream.resolve_enum(MachO.DyldInfoCommand.RebaseData.Opcode, (self.opcode_and_immediate & 240)) 1557 | return getattr(self, '_m_opcode', None) 1558 | 1559 | @property 1560 | def immediate(self): 1561 | if hasattr(self, '_m_immediate'): 1562 | return self._m_immediate 1563 | 1564 | self._m_immediate = (self.opcode_and_immediate & 15) 1565 | return getattr(self, '_m_immediate', None) 1566 | 1567 | 1568 | 1569 | class ExportNode(KaitaiStruct): 1570 | def __init__(self, _io, _parent=None, _root=None): 1571 | self._io = _io 1572 | self._parent = _parent 1573 | self._root = _root if _root else self 1574 | self._read() 1575 | 1576 | def _read(self): 1577 | self.terminal_size = MachO.Uleb128(self._io, self, self._root) 1578 | self.children_count = self._io.read_u1() 1579 | self.children = [] 1580 | for i in range(self.children_count): 1581 | self.children.append(MachO.DyldInfoCommand.ExportNode.Child(self._io, self, self._root)) 1582 | 1583 | self.terminal = self._io.read_bytes(self.terminal_size.value) 1584 | 1585 | class Child(KaitaiStruct): 1586 | def __init__(self, _io, _parent=None, _root=None): 1587 | self._io = _io 1588 | self._parent = _parent 1589 | self._root = _root if _root else self 1590 | self._read() 1591 | 1592 | def _read(self): 1593 | self.name = (self._io.read_bytes_term(0, False, True, True)).decode(u"ascii") 1594 | self.node_offset = MachO.Uleb128(self._io, self, self._root) 1595 | 1596 | @property 1597 | def value(self): 1598 | if hasattr(self, '_m_value'): 1599 | return self._m_value 1600 | 1601 | _pos = self._io.pos() 1602 | self._io.seek(self.node_offset.value) 1603 | self._m_value = MachO.DyldInfoCommand.ExportNode(self._io, self, self._root) 1604 | self._io.seek(_pos) 1605 | return getattr(self, '_m_value', None) 1606 | 1607 | 1608 | 1609 | class BindData(KaitaiStruct): 1610 | def __init__(self, _io, _parent=None, _root=None): 1611 | self._io = _io 1612 | self._parent = _parent 1613 | self._root = _root if _root else self 1614 | self._read() 1615 | 1616 | def _read(self): 1617 | self.items = [] 1618 | i = 0 1619 | while True: 1620 | _ = MachO.DyldInfoCommand.BindItem(self._io, self, self._root) 1621 | self.items.append(_) 1622 | if _.opcode == MachO.DyldInfoCommand.BindOpcode.done: 1623 | break 1624 | i += 1 1625 | 1626 | 1627 | class LazyBindData(KaitaiStruct): 1628 | def __init__(self, _io, _parent=None, _root=None): 1629 | self._io = _io 1630 | self._parent = _parent 1631 | self._root = _root if _root else self 1632 | self._read() 1633 | 1634 | def _read(self): 1635 | self.items = [] 1636 | i = 0 1637 | while not self._io.is_eof(): 1638 | self.items.append(MachO.DyldInfoCommand.BindItem(self._io, self, self._root)) 1639 | i += 1 1640 | 1641 | 1642 | 1643 | @property 1644 | def rebase(self): 1645 | if hasattr(self, '_m_rebase'): 1646 | return self._m_rebase 1647 | 1648 | io = self._root._io 1649 | _pos = io.pos() 1650 | io.seek(self.rebase_off) 1651 | self._raw__m_rebase = io.read_bytes(self.rebase_size) 1652 | _io__raw__m_rebase = KaitaiStream(BytesIO(self._raw__m_rebase)) 1653 | self._m_rebase = MachO.DyldInfoCommand.RebaseData(_io__raw__m_rebase, self, self._root) 1654 | io.seek(_pos) 1655 | return getattr(self, '_m_rebase', None) 1656 | 1657 | @property 1658 | def bind(self): 1659 | if hasattr(self, '_m_bind'): 1660 | return self._m_bind 1661 | 1662 | io = self._root._io 1663 | _pos = io.pos() 1664 | io.seek(self.bind_off) 1665 | self._raw__m_bind = io.read_bytes(self.bind_size) 1666 | _io__raw__m_bind = KaitaiStream(BytesIO(self._raw__m_bind)) 1667 | self._m_bind = MachO.DyldInfoCommand.BindData(_io__raw__m_bind, self, self._root) 1668 | io.seek(_pos) 1669 | return getattr(self, '_m_bind', None) 1670 | 1671 | @property 1672 | def lazy_bind(self): 1673 | if hasattr(self, '_m_lazy_bind'): 1674 | return self._m_lazy_bind 1675 | 1676 | io = self._root._io 1677 | _pos = io.pos() 1678 | io.seek(self.lazy_bind_off) 1679 | self._raw__m_lazy_bind = io.read_bytes(self.lazy_bind_size) 1680 | _io__raw__m_lazy_bind = KaitaiStream(BytesIO(self._raw__m_lazy_bind)) 1681 | self._m_lazy_bind = MachO.DyldInfoCommand.LazyBindData(_io__raw__m_lazy_bind, self, self._root) 1682 | io.seek(_pos) 1683 | return getattr(self, '_m_lazy_bind', None) 1684 | 1685 | @property 1686 | def exports(self): 1687 | if hasattr(self, '_m_exports'): 1688 | return self._m_exports 1689 | 1690 | io = self._root._io 1691 | _pos = io.pos() 1692 | io.seek(self.export_off) 1693 | self._raw__m_exports = io.read_bytes(self.export_size) 1694 | _io__raw__m_exports = KaitaiStream(BytesIO(self._raw__m_exports)) 1695 | self._m_exports = MachO.DyldInfoCommand.ExportNode(_io__raw__m_exports, self, self._root) 1696 | io.seek(_pos) 1697 | return getattr(self, '_m_exports', None) 1698 | 1699 | 1700 | class DylinkerCommand(KaitaiStruct): 1701 | def __init__(self, _io, _parent=None, _root=None): 1702 | self._io = _io 1703 | self._parent = _parent 1704 | self._root = _root if _root else self 1705 | self._read() 1706 | 1707 | def _read(self): 1708 | self.name = MachO.LcStr(self._io, self, self._root) 1709 | 1710 | 1711 | class DylibCommand(KaitaiStruct): 1712 | def __init__(self, _io, _parent=None, _root=None): 1713 | self._io = _io 1714 | self._parent = _parent 1715 | self._root = _root if _root else self 1716 | self._read() 1717 | 1718 | def _read(self): 1719 | self.name_offset = self._io.read_u4le() 1720 | self.timestamp = self._io.read_u4le() 1721 | self.current_version = self._io.read_u4le() 1722 | self.compatibility_version = self._io.read_u4le() 1723 | self.name = (self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8") 1724 | 1725 | 1726 | class SegmentCommand(KaitaiStruct): 1727 | def __init__(self, _io, _parent=None, _root=None): 1728 | self._io = _io 1729 | self._parent = _parent 1730 | self._root = _root if _root else self 1731 | self._read() 1732 | 1733 | def _read(self): 1734 | self.segname = (KaitaiStream.bytes_strip_right(self._io.read_bytes(16), 0)).decode(u"ascii") 1735 | self.vmaddr = self._io.read_u4le() 1736 | self.vmsize = self._io.read_u4le() 1737 | self.fileoff = self._io.read_u4le() 1738 | self.filesize = self._io.read_u4le() 1739 | self.maxprot = MachO.VmProt(self._io, self, self._root) 1740 | self.initprot = MachO.VmProt(self._io, self, self._root) 1741 | self.nsects = self._io.read_u4le() 1742 | self.flags = self._io.read_u4le() 1743 | self.sections = [] 1744 | for i in range(self.nsects): 1745 | self.sections.append(MachO.SegmentCommand.Section(self._io, self, self._root)) 1746 | 1747 | 1748 | class Section(KaitaiStruct): 1749 | def __init__(self, _io, _parent=None, _root=None): 1750 | self._io = _io 1751 | self._parent = _parent 1752 | self._root = _root if _root else self 1753 | self._read() 1754 | 1755 | def _read(self): 1756 | self.sect_name = (KaitaiStream.bytes_strip_right(self._io.read_bytes(16), 0)).decode(u"ascii") 1757 | self.seg_name = (KaitaiStream.bytes_strip_right(self._io.read_bytes(16), 0)).decode(u"ascii") 1758 | self.addr = self._io.read_u4le() 1759 | self.size = self._io.read_u4le() 1760 | self.offset = self._io.read_u4le() 1761 | self.align = self._io.read_u4le() 1762 | self.reloff = self._io.read_u4le() 1763 | self.nreloc = self._io.read_u4le() 1764 | self.flags = self._io.read_u4le() 1765 | self.reserved1 = self._io.read_u4le() 1766 | self.reserved2 = self._io.read_u4le() 1767 | 1768 | @property 1769 | def data(self): 1770 | if hasattr(self, '_m_data'): 1771 | return self._m_data 1772 | 1773 | io = self._root._io 1774 | _pos = io.pos() 1775 | io.seek(self.offset) 1776 | self._m_data = io.read_bytes(self.size) 1777 | io.seek(_pos) 1778 | return getattr(self, '_m_data', None) 1779 | 1780 | 1781 | 1782 | class LcStr(KaitaiStruct): 1783 | def __init__(self, _io, _parent=None, _root=None): 1784 | self._io = _io 1785 | self._parent = _parent 1786 | self._root = _root if _root else self 1787 | self._read() 1788 | 1789 | def _read(self): 1790 | self.length = self._io.read_u4le() 1791 | self.value = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8") 1792 | 1793 | 1794 | class LoadCommand(KaitaiStruct): 1795 | def __init__(self, _io, _parent=None, _root=None): 1796 | self._io = _io 1797 | self._parent = _parent 1798 | self._root = _root if _root else self 1799 | self._read() 1800 | 1801 | def _read(self): 1802 | self.type = KaitaiStream.resolve_enum(MachO.LoadCommandType, self._io.read_u4le()) 1803 | self.size = self._io.read_u4le() 1804 | _on = self.type 1805 | if _on == MachO.LoadCommandType.id_dylinker: 1806 | self._raw_body = self._io.read_bytes((self.size - 8)) 1807 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1808 | self.body = MachO.DylinkerCommand(_io__raw_body, self, self._root) 1809 | elif _on == MachO.LoadCommandType.reexport_dylib: 1810 | self._raw_body = self._io.read_bytes((self.size - 8)) 1811 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1812 | self.body = MachO.DylibCommand(_io__raw_body, self, self._root) 1813 | elif _on == MachO.LoadCommandType.build_version: 1814 | self._raw_body = self._io.read_bytes((self.size - 8)) 1815 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1816 | self.body = MachO.BuildVersionCommand(_io__raw_body, self, self._root) 1817 | elif _on == MachO.LoadCommandType.source_version: 1818 | self._raw_body = self._io.read_bytes((self.size - 8)) 1819 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1820 | self.body = MachO.SourceVersionCommand(_io__raw_body, self, self._root) 1821 | elif _on == MachO.LoadCommandType.function_starts: 1822 | self._raw_body = self._io.read_bytes((self.size - 8)) 1823 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1824 | self.body = MachO.LinkeditDataCommand(_io__raw_body, self, self._root) 1825 | elif _on == MachO.LoadCommandType.rpath: 1826 | self._raw_body = self._io.read_bytes((self.size - 8)) 1827 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1828 | self.body = MachO.RpathCommand(_io__raw_body, self, self._root) 1829 | elif _on == MachO.LoadCommandType.sub_framework: 1830 | self._raw_body = self._io.read_bytes((self.size - 8)) 1831 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1832 | self.body = MachO.SubCommand(_io__raw_body, self, self._root) 1833 | elif _on == MachO.LoadCommandType.routines: 1834 | self._raw_body = self._io.read_bytes((self.size - 8)) 1835 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1836 | self.body = MachO.RoutinesCommand(_io__raw_body, self, self._root) 1837 | elif _on == MachO.LoadCommandType.sub_library: 1838 | self._raw_body = self._io.read_bytes((self.size - 8)) 1839 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1840 | self.body = MachO.SubCommand(_io__raw_body, self, self._root) 1841 | elif _on == MachO.LoadCommandType.dyld_info_only: 1842 | self._raw_body = self._io.read_bytes((self.size - 8)) 1843 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1844 | self.body = MachO.DyldInfoCommand(_io__raw_body, self, self._root) 1845 | elif _on == MachO.LoadCommandType.dyld_environment: 1846 | self._raw_body = self._io.read_bytes((self.size - 8)) 1847 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1848 | self.body = MachO.DylinkerCommand(_io__raw_body, self, self._root) 1849 | elif _on == MachO.LoadCommandType.load_dylinker: 1850 | self._raw_body = self._io.read_bytes((self.size - 8)) 1851 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1852 | self.body = MachO.DylinkerCommand(_io__raw_body, self, self._root) 1853 | elif _on == MachO.LoadCommandType.segment_split_info: 1854 | self._raw_body = self._io.read_bytes((self.size - 8)) 1855 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1856 | self.body = MachO.LinkeditDataCommand(_io__raw_body, self, self._root) 1857 | elif _on == MachO.LoadCommandType.main: 1858 | self._raw_body = self._io.read_bytes((self.size - 8)) 1859 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1860 | self.body = MachO.EntryPointCommand(_io__raw_body, self, self._root) 1861 | elif _on == MachO.LoadCommandType.load_dylib: 1862 | self._raw_body = self._io.read_bytes((self.size - 8)) 1863 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1864 | self.body = MachO.DylibCommand(_io__raw_body, self, self._root) 1865 | elif _on == MachO.LoadCommandType.encryption_info: 1866 | self._raw_body = self._io.read_bytes((self.size - 8)) 1867 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1868 | self.body = MachO.EncryptionInfoCommand(_io__raw_body, self, self._root) 1869 | elif _on == MachO.LoadCommandType.dysymtab: 1870 | self._raw_body = self._io.read_bytes((self.size - 8)) 1871 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1872 | self.body = MachO.DysymtabCommand(_io__raw_body, self, self._root) 1873 | elif _on == MachO.LoadCommandType.twolevel_hints: 1874 | self._raw_body = self._io.read_bytes((self.size - 8)) 1875 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1876 | self.body = MachO.TwolevelHintsCommand(_io__raw_body, self, self._root) 1877 | elif _on == MachO.LoadCommandType.encryption_info_64: 1878 | self._raw_body = self._io.read_bytes((self.size - 8)) 1879 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1880 | self.body = MachO.EncryptionInfoCommand(_io__raw_body, self, self._root) 1881 | elif _on == MachO.LoadCommandType.linker_option: 1882 | self._raw_body = self._io.read_bytes((self.size - 8)) 1883 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1884 | self.body = MachO.LinkerOptionCommand(_io__raw_body, self, self._root) 1885 | elif _on == MachO.LoadCommandType.dyld_info: 1886 | self._raw_body = self._io.read_bytes((self.size - 8)) 1887 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1888 | self.body = MachO.DyldInfoCommand(_io__raw_body, self, self._root) 1889 | elif _on == MachO.LoadCommandType.version_min_tvos: 1890 | self._raw_body = self._io.read_bytes((self.size - 8)) 1891 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1892 | self.body = MachO.VersionMinCommand(_io__raw_body, self, self._root) 1893 | elif _on == MachO.LoadCommandType.load_upward_dylib: 1894 | self._raw_body = self._io.read_bytes((self.size - 8)) 1895 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1896 | self.body = MachO.DylibCommand(_io__raw_body, self, self._root) 1897 | elif _on == MachO.LoadCommandType.segment_64: 1898 | self._raw_body = self._io.read_bytes((self.size - 8)) 1899 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1900 | self.body = MachO.SegmentCommand64(_io__raw_body, self, self._root) 1901 | elif _on == MachO.LoadCommandType.segment: 1902 | self._raw_body = self._io.read_bytes((self.size - 8)) 1903 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1904 | self.body = MachO.SegmentCommand(_io__raw_body, self, self._root) 1905 | elif _on == MachO.LoadCommandType.sub_umbrella: 1906 | self._raw_body = self._io.read_bytes((self.size - 8)) 1907 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1908 | self.body = MachO.SubCommand(_io__raw_body, self, self._root) 1909 | elif _on == MachO.LoadCommandType.version_min_watchos: 1910 | self._raw_body = self._io.read_bytes((self.size - 8)) 1911 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1912 | self.body = MachO.VersionMinCommand(_io__raw_body, self, self._root) 1913 | elif _on == MachO.LoadCommandType.routines_64: 1914 | self._raw_body = self._io.read_bytes((self.size - 8)) 1915 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1916 | self.body = MachO.RoutinesCommand64(_io__raw_body, self, self._root) 1917 | elif _on == MachO.LoadCommandType.id_dylib: 1918 | self._raw_body = self._io.read_bytes((self.size - 8)) 1919 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1920 | self.body = MachO.DylibCommand(_io__raw_body, self, self._root) 1921 | elif _on == MachO.LoadCommandType.sub_client: 1922 | self._raw_body = self._io.read_bytes((self.size - 8)) 1923 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1924 | self.body = MachO.SubCommand(_io__raw_body, self, self._root) 1925 | elif _on == MachO.LoadCommandType.dylib_code_sign_drs: 1926 | self._raw_body = self._io.read_bytes((self.size - 8)) 1927 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1928 | self.body = MachO.LinkeditDataCommand(_io__raw_body, self, self._root) 1929 | elif _on == MachO.LoadCommandType.symtab: 1930 | self._raw_body = self._io.read_bytes((self.size - 8)) 1931 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1932 | self.body = MachO.SymtabCommand(_io__raw_body, self, self._root) 1933 | elif _on == MachO.LoadCommandType.linker_optimization_hint: 1934 | self._raw_body = self._io.read_bytes((self.size - 8)) 1935 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1936 | self.body = MachO.LinkeditDataCommand(_io__raw_body, self, self._root) 1937 | elif _on == MachO.LoadCommandType.data_in_code: 1938 | self._raw_body = self._io.read_bytes((self.size - 8)) 1939 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1940 | self.body = MachO.LinkeditDataCommand(_io__raw_body, self, self._root) 1941 | elif _on == MachO.LoadCommandType.code_signature: 1942 | self._raw_body = self._io.read_bytes((self.size - 8)) 1943 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1944 | self.body = MachO.CodeSignatureCommand(_io__raw_body, self, self._root) 1945 | elif _on == MachO.LoadCommandType.version_min_iphoneos: 1946 | self._raw_body = self._io.read_bytes((self.size - 8)) 1947 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1948 | self.body = MachO.VersionMinCommand(_io__raw_body, self, self._root) 1949 | elif _on == MachO.LoadCommandType.load_weak_dylib: 1950 | self._raw_body = self._io.read_bytes((self.size - 8)) 1951 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1952 | self.body = MachO.DylibCommand(_io__raw_body, self, self._root) 1953 | elif _on == MachO.LoadCommandType.lazy_load_dylib: 1954 | self._raw_body = self._io.read_bytes((self.size - 8)) 1955 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1956 | self.body = MachO.DylibCommand(_io__raw_body, self, self._root) 1957 | elif _on == MachO.LoadCommandType.uuid: 1958 | self._raw_body = self._io.read_bytes((self.size - 8)) 1959 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1960 | self.body = MachO.UuidCommand(_io__raw_body, self, self._root) 1961 | elif _on == MachO.LoadCommandType.version_min_macosx: 1962 | self._raw_body = self._io.read_bytes((self.size - 8)) 1963 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 1964 | self.body = MachO.VersionMinCommand(_io__raw_body, self, self._root) 1965 | else: 1966 | self.body = self._io.read_bytes((self.size - 8)) 1967 | 1968 | 1969 | class UuidCommand(KaitaiStruct): 1970 | def __init__(self, _io, _parent=None, _root=None): 1971 | self._io = _io 1972 | self._parent = _parent 1973 | self._root = _root if _root else self 1974 | self._read() 1975 | 1976 | def _read(self): 1977 | self.uuid = self._io.read_bytes(16) 1978 | 1979 | 1980 | class SymtabCommand(KaitaiStruct): 1981 | def __init__(self, _io, _parent=None, _root=None): 1982 | self._io = _io 1983 | self._parent = _parent 1984 | self._root = _root if _root else self 1985 | self._read() 1986 | 1987 | def _read(self): 1988 | self.sym_off = self._io.read_u4le() 1989 | self.n_syms = self._io.read_u4le() 1990 | self.str_off = self._io.read_u4le() 1991 | self.str_size = self._io.read_u4le() 1992 | 1993 | class StrTable(KaitaiStruct): 1994 | def __init__(self, _io, _parent=None, _root=None): 1995 | self._io = _io 1996 | self._parent = _parent 1997 | self._root = _root if _root else self 1998 | self._read() 1999 | 2000 | def _read(self): 2001 | self.unknown = self._io.read_u4le() 2002 | self.items = [] 2003 | i = 0 2004 | while True: 2005 | _ = (self._io.read_bytes_term(0, False, True, False)).decode(u"utf-8") 2006 | self.items.append(_) 2007 | if _ == u"": 2008 | break 2009 | i += 1 2010 | 2011 | 2012 | class Nlist64(KaitaiStruct): 2013 | def __init__(self, _io, _parent=None, _root=None): 2014 | self._io = _io 2015 | self._parent = _parent 2016 | self._root = _root if _root else self 2017 | self._read() 2018 | 2019 | def _read(self): 2020 | self.un = self._io.read_u4le() 2021 | self.type = self._io.read_u1() 2022 | self.sect = self._io.read_u1() 2023 | self.desc = self._io.read_u2le() 2024 | self.value = self._io.read_u8le() 2025 | 2026 | @property 2027 | def name(self): 2028 | if hasattr(self, '_m_name'): 2029 | return self._m_name 2030 | 2031 | if self.un != 0: 2032 | _pos = self._io.pos() 2033 | self._io.seek((self._parent.str_off + self.un)) 2034 | self._m_name = (self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8") 2035 | self._io.seek(_pos) 2036 | 2037 | return getattr(self, '_m_name', None) 2038 | 2039 | 2040 | class Nlist(KaitaiStruct): 2041 | def __init__(self, _io, _parent=None, _root=None): 2042 | self._io = _io 2043 | self._parent = _parent 2044 | self._root = _root if _root else self 2045 | self._read() 2046 | 2047 | def _read(self): 2048 | self.un = self._io.read_u4le() 2049 | self.type = self._io.read_u1() 2050 | self.sect = self._io.read_u1() 2051 | self.desc = self._io.read_u2le() 2052 | self.value = self._io.read_u4le() 2053 | 2054 | @property 2055 | def name(self): 2056 | if hasattr(self, '_m_name'): 2057 | return self._m_name 2058 | 2059 | if self.un != 0: 2060 | _pos = self._io.pos() 2061 | self._io.seek((self._parent.str_off + self.un)) 2062 | self._m_name = (self._io.read_bytes_term(0, False, True, True)).decode(u"utf-8") 2063 | self._io.seek(_pos) 2064 | 2065 | return getattr(self, '_m_name', None) 2066 | 2067 | 2068 | @property 2069 | def symbols(self): 2070 | if hasattr(self, '_m_symbols'): 2071 | return self._m_symbols 2072 | 2073 | io = self._root._io 2074 | _pos = io.pos() 2075 | io.seek(self.sym_off) 2076 | self._m_symbols = [] 2077 | for i in range(self.n_syms): 2078 | _on = self._root.magic 2079 | if _on == MachO.MagicType.macho_le_x64: 2080 | self._m_symbols.append(MachO.SymtabCommand.Nlist64(io, self, self._root)) 2081 | elif _on == MachO.MagicType.macho_be_x64: 2082 | self._m_symbols.append(MachO.SymtabCommand.Nlist64(io, self, self._root)) 2083 | elif _on == MachO.MagicType.macho_le_x86: 2084 | self._m_symbols.append(MachO.SymtabCommand.Nlist(io, self, self._root)) 2085 | elif _on == MachO.MagicType.macho_be_x86: 2086 | self._m_symbols.append(MachO.SymtabCommand.Nlist(io, self, self._root)) 2087 | 2088 | io.seek(_pos) 2089 | return getattr(self, '_m_symbols', None) 2090 | 2091 | @property 2092 | def strs(self): 2093 | if hasattr(self, '_m_strs'): 2094 | return self._m_strs 2095 | 2096 | io = self._root._io 2097 | _pos = io.pos() 2098 | io.seek(self.str_off) 2099 | self._raw__m_strs = io.read_bytes(self.str_size) 2100 | _io__raw__m_strs = KaitaiStream(BytesIO(self._raw__m_strs)) 2101 | self._m_strs = MachO.SymtabCommand.StrTable(_io__raw__m_strs, self, self._root) 2102 | io.seek(_pos) 2103 | return getattr(self, '_m_strs', None) 2104 | 2105 | 2106 | class VersionMinCommand(KaitaiStruct): 2107 | def __init__(self, _io, _parent=None, _root=None): 2108 | self._io = _io 2109 | self._parent = _parent 2110 | self._root = _root if _root else self 2111 | self._read() 2112 | 2113 | def _read(self): 2114 | self.version = MachO.Version(self._io, self, self._root) 2115 | self.sdk = MachO.Version(self._io, self, self._root) 2116 | 2117 | 2118 | class EntryPointCommand(KaitaiStruct): 2119 | def __init__(self, _io, _parent=None, _root=None): 2120 | self._io = _io 2121 | self._parent = _parent 2122 | self._root = _root if _root else self 2123 | self._read() 2124 | 2125 | def _read(self): 2126 | self.entry_off = self._io.read_u8le() 2127 | self.stack_size = self._io.read_u8le() 2128 | 2129 | 2130 | 2131 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "abi3audit" 7 | dynamic = ["version"] 8 | description = "Scans Python wheels for abi3 violations and inconsistencies" 9 | readme = "README.md" 10 | license = { file = "LICENSE" } 11 | authors = [{ name = "William Woodruff", email = "william@trailofbits.com" }] 12 | classifiers = [ 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3 :: Only", 15 | "Programming Language :: Python :: 3", 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Developers", 18 | "Topic :: Security", 19 | ] 20 | dependencies = [ 21 | "abi3info >= 2024.06.19", 22 | "kaitaistruct ~= 0.10", 23 | "packaging >= 21.3,< 26.0", 24 | "pefile >= 2022.5.30", 25 | "pyelftools >= 0.29", 26 | "requests >= 2.28.1,< 2.33.0", 27 | "requests-cache >= 0.9.6,< 1.3.0", 28 | "rich >= 12.5.1,< 14.1.0", 29 | ] 30 | requires-python = ">=3.9" 31 | 32 | [project.urls] 33 | Homepage = "https://pypi.org/project/abi3audit/" 34 | Issues = "https://github.com/pypa/abi3audit/issues" 35 | Source = "https://github.com/pypa/abi3audit" 36 | 37 | [project.optional-dependencies] 38 | test = ["pytest", "pytest-cov", "pretend", "coverage[toml]"] 39 | lint = ["bandit", "interrogate", "mypy", "ruff", "types-requests"] 40 | dev = ["build", "pdoc3", "abi3audit[test,lint]"] 41 | 42 | [project.scripts] 43 | abi3audit = "abi3audit._cli:main" 44 | 45 | [tool.interrogate] 46 | exclude = ["env", "test", "codegen"] 47 | ignore-semiprivate = true 48 | fail-under = 100 49 | 50 | [tool.mypy] 51 | allow_redefinition = true 52 | check_untyped_defs = true 53 | disallow_incomplete_defs = true 54 | disallow_untyped_defs = true 55 | exclude = ["_vendor/"] 56 | ignore_missing_imports = true 57 | no_implicit_optional = true 58 | show_error_codes = true 59 | strict_equality = true 60 | warn_no_return = true 61 | warn_redundant_casts = true 62 | warn_return_any = true 63 | warn_unreachable = true 64 | warn_unused_configs = true 65 | warn_unused_ignores = true 66 | 67 | [tool.bandit] 68 | exclude_dirs = ["./test"] 69 | 70 | [tool.coverage.run] 71 | omit = ["abi3audit/_vendor/*"] 72 | 73 | [tool.ruff] 74 | line-length = 100 75 | 76 | [tool.ruff.lint] 77 | select = ["E", "F", "I", "W", "UP"] 78 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/abi3audit/bb0a20730e2dd39524de14b34ff63e72224b40c7/test/__init__.py -------------------------------------------------------------------------------- /test/test_audit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import hashlib 4 | import logging 5 | from collections.abc import Iterable 6 | from dataclasses import dataclass 7 | from html.parser import HTMLParser 8 | from pathlib import Path 9 | 10 | import pytest 11 | import requests 12 | 13 | from abi3audit._audit import audit 14 | from abi3audit._extract import make_specs 15 | 16 | logger = logging.getLogger("abi3audit-tests") 17 | 18 | 19 | WHEELHOUSE = Path(__file__).parent / "wheelhouse" 20 | WHEELHOUSE.mkdir(exist_ok=True) 21 | 22 | 23 | _PACKAGE = "cryptography" 24 | _VERSION = "42.0.7" 25 | _PY = ("cp37", "cp39") 26 | _PLATFORMS = ( 27 | "macosx_10_12_universal2", 28 | "manylinux_2_17_aarch64.manylinux2014_aarch64", 29 | "manylinux_2_17_x86_64.manylinux2014_x86_64", 30 | "manylinux_2_28_aarch64", 31 | "manylinux_2_28_x86_64", 32 | "musllinux_1_1_aarch64", 33 | "musllinux_1_1_x86_64", 34 | "musllinux_1_2_aarch64", 35 | "musllinux_1_2_x86_64", 36 | "win32", 37 | "win_amd64", 38 | ) 39 | 40 | 41 | @dataclass(frozen=True, unsafe_hash=True) 42 | class Wheel: 43 | package: str 44 | version: str 45 | python: str 46 | platform: str 47 | 48 | def download(self, url: str, integrity: str, dest: Path = WHEELHOUSE) -> Path: 49 | wheel = dest / self.tag() 50 | if wheel.exists(): 51 | logger.debug(f"serving wheel {self.tag()} directly from local path {dest}") 52 | return wheel 53 | 54 | logger.debug(f"downloading wheel {self.tag()} from URL {url} into {dest}.") 55 | resp = requests.get(url, stream=True) 56 | resp.raise_for_status() 57 | h = hashlib.sha256() 58 | with open(wheel, "wb") as wfd: 59 | for chunk in resp.iter_content(chunk_size=16 * 1024): 60 | h.update(chunk) 61 | wfd.write(chunk) 62 | 63 | hashval = h.hexdigest() 64 | if hashval != integrity: 65 | wheel.unlink() 66 | raise RuntimeError( 67 | f"SHA-256 hash value mismatch: expected hash {integrity}, got {hashval}" 68 | ) 69 | return wheel 70 | 71 | def tag(self) -> str: 72 | return f"{self.package}-{self.version}-{self.python}-abi3-{self.platform}.whl" 73 | 74 | def __str__(self): 75 | return self.tag() 76 | 77 | 78 | class WheelLinkParser(HTMLParser): 79 | def __init__(self, wheels: Iterable[Wheel]): 80 | super().__init__() 81 | self.wheels = {w.tag(): w for w in wheels} 82 | 83 | def handle_starttag(self, tag: str, attrs: list[tuple[str, str]]): 84 | if tag != "a" or not attrs: 85 | return 86 | # href attribute is always first on PyPI simple anchor tags. 87 | _, url = attrs[0] 88 | wheel_and_sha = url.rsplit("/", 1)[1] 89 | # URLs have the format /$WHEELTAG#sha256=$SHASUM. 90 | wheeltag, integrity = wheel_and_sha.split("#", 1) 91 | # split off the leading "sha256=". 92 | integrity = integrity[7:] 93 | 94 | if wheeltag in self.wheels: 95 | self.wheels[wheeltag].download(url=url, integrity=integrity) 96 | 97 | 98 | @pytest.mark.parametrize( 99 | "wheel", 100 | [Wheel(_PACKAGE, _VERSION, py, platform) for py in _PY for platform in _PLATFORMS], 101 | ids=str, 102 | ) 103 | def test_audit_results_on_golden_wheels(wheel): 104 | wheel_downloader = WheelLinkParser([wheel]) 105 | simple_cryptography_pypi_page = requests.get(f"https://pypi.org/simple/{_PACKAGE}/") 106 | simple_cryptography_pypi_page.raise_for_status() 107 | # downloads the wheels when finding the appropriate URLs in the HTML doc. 108 | wheel_downloader.feed(simple_cryptography_pypi_page.content.decode()) 109 | 110 | spec = make_specs(str(WHEELHOUSE / wheel.tag()))[0] 111 | for so in spec._extractor(): 112 | # a trimmed version of the loop in abi3audit._cli.main(). 113 | result = audit(so) 114 | assert bool(result), f"got non-abi3 symbols {result.non_abi3_symbols}" 115 | -------------------------------------------------------------------------------- /test/test_extract.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from abi3info.models import PyVersion 3 | 4 | from abi3audit._extract import ( 5 | ExtractorError, 6 | InvalidSpec, 7 | PyPISpec, 8 | SharedObjectSpec, 9 | WheelExtractor, 10 | WheelSpec, 11 | make_specs, 12 | ) 13 | 14 | 15 | def test_make_spec(): 16 | assert make_specs("foo.whl") == [WheelSpec("foo.whl")] 17 | assert make_specs("foo.abi3.so") == [SharedObjectSpec("foo.abi3.so")] 18 | assert make_specs("foo") == [PyPISpec("foo")] 19 | 20 | # Shared objects need to be tagged with `.abi3` or --assume-minimum-abi3 21 | # must be used. 22 | with pytest.raises(InvalidSpec, match="--assume-minimum-abi3"): 23 | make_specs("foo.so") 24 | 25 | make_specs("foo.so", assume_minimum_abi3=PyVersion(3, 2)) == [SharedObjectSpec("foo.so")] 26 | 27 | # Anything that doesn't look like a wheel, shared object, or PyPI package fails. 28 | with pytest.raises(InvalidSpec): 29 | make_specs("foo?") 30 | 31 | 32 | class TestWheelExtractor: 33 | def test_init(self): 34 | with pytest.raises(ExtractorError): 35 | WheelExtractor(WheelSpec("not-a-real-file")) 36 | -------------------------------------------------------------------------------- /test/test_minimum_version.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from abi3audit._cli import _PyVersionAction 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "version, ok", 8 | ( 9 | ("3.3", True), 10 | ("3.12", True), 11 | ("3.a", False), 12 | ("2.7", False), 13 | ("3.1", False), 14 | ("4.0", False), 15 | ("5", False), 16 | ("3", False), 17 | ("3.7.1", False), 18 | ), 19 | ) 20 | def test_ensure_version(version, ok): 21 | if ok: 22 | pyversion = _PyVersionAction._ensure_pyversion(version) 23 | assert pyversion.major == 3 24 | assert pyversion.minor >= 2 25 | else: 26 | with pytest.raises(ValueError): 27 | _PyVersionAction._ensure_pyversion(version) 28 | --------------------------------------------------------------------------------