├── Brewfile ├── .yardopts ├── test ├── bin │ ├── i386 │ │ ├── hello.o │ │ ├── hello.bin │ │ ├── hellobundle.so │ │ ├── libhello.dylib │ │ ├── hello_expected.bin │ │ ├── libextrahello.dylib │ │ ├── hello_rpath_expected.bin │ │ └── libhello_expected.dylib │ ├── ppc │ │ ├── hello.o │ │ ├── hello.bin │ │ ├── hellobundle.so │ │ ├── libhello.dylib │ │ ├── hello_expected.bin │ │ ├── libextrahello.dylib │ │ ├── libhello_expected.dylib │ │ └── hello_rpath_expected.bin │ ├── x86_64 │ │ ├── hello.o │ │ ├── hello.bin │ │ ├── libdupe.dylib │ │ ├── hellobundle.so │ │ ├── libhello.dylib │ │ ├── hello_expected.bin │ │ ├── hello_unk_lc.bin │ │ ├── libextrahello.dylib │ │ ├── libhello_expected.dylib │ │ ├── hello_rpath_expected.bin │ │ └── dylib_use_command-weak-delay.bin │ ├── fat-i386-ppc │ │ ├── hello.o │ │ ├── hello.bin │ │ ├── hellobundle.so │ │ ├── libhello.dylib │ │ ├── hello_expected.bin │ │ ├── libextrahello.dylib │ │ ├── hello_rpath_expected.bin │ │ └── libhello_expected.dylib │ ├── fat-i386-x86_64 │ │ ├── hello.bin │ │ ├── hello.o │ │ ├── hellobundle.so │ │ ├── libhello.dylib │ │ ├── hello_expected.bin │ │ ├── libextrahello.dylib │ │ ├── libinconsistent.dylib │ │ ├── hello_rpath_expected.bin │ │ └── libhello_expected.dylib │ └── llvm │ │ ├── macho-invalid-fat-header │ │ ├── macho-invalid-fat_cputype │ │ └── LICENSE.txt ├── src │ ├── libhello.c │ ├── hello.c │ ├── make-inconsistent.sh │ └── Makefile ├── test_kc.rb ├── test_open.rb ├── helpers.rb ├── test_create_load_commands.rb ├── test_structure_dsl.rb ├── test_serialize_load_commands.rb ├── bench.rb ├── test_tools.rb ├── test_fat.rb └── test_macho.rb ├── .github ├── zizmor.yml ├── codeql │ └── extensions │ │ └── homebrew-actions.yml ├── dependabot.yml └── workflows │ ├── release.yml │ ├── tests.yml │ ├── stale-issues.yml │ └── actionlint.yml ├── .editorconfig ├── Gemfile ├── Rakefile ├── .gitignore ├── ruby-macho.gemspec ├── .overcommit.yml ├── run-tests ├── LICENSE ├── lib ├── macho │ ├── view.rb │ ├── tools.rb │ ├── utils.rb │ ├── sections.rb │ ├── exceptions.rb │ ├── structure.rb │ ├── fat_file.rb │ └── headers.rb └── macho.rb ├── .rubocop.yml ├── Gemfile.lock ├── README.md └── machostructure-dsl-docs.md /Brewfile: -------------------------------------------------------------------------------- 1 | brew "ruby" 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private --markup-provider=redcarpet --markup=markdown - README.md LICENSE 2 | -------------------------------------------------------------------------------- /test/bin/i386/hello.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/hello.o -------------------------------------------------------------------------------- /test/bin/ppc/hello.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/hello.o -------------------------------------------------------------------------------- /test/bin/i386/hello.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/hello.bin -------------------------------------------------------------------------------- /test/bin/ppc/hello.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/hello.bin -------------------------------------------------------------------------------- /test/bin/x86_64/hello.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/hello.o -------------------------------------------------------------------------------- /test/bin/x86_64/hello.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/hello.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/hello.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/hello.o -------------------------------------------------------------------------------- /test/bin/i386/hellobundle.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/hellobundle.so -------------------------------------------------------------------------------- /test/bin/i386/libhello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/libhello.dylib -------------------------------------------------------------------------------- /test/bin/ppc/hellobundle.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/hellobundle.so -------------------------------------------------------------------------------- /test/bin/ppc/libhello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/libhello.dylib -------------------------------------------------------------------------------- /test/bin/x86_64/libdupe.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/libdupe.dylib -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/hello.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/hello.bin -------------------------------------------------------------------------------- /test/bin/ppc/hello_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/hello_expected.bin -------------------------------------------------------------------------------- /test/bin/x86_64/hellobundle.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/hellobundle.so -------------------------------------------------------------------------------- /test/bin/x86_64/libhello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/libhello.dylib -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/hello.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/hello.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/hello.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/hello.o -------------------------------------------------------------------------------- /test/bin/i386/hello_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/hello_expected.bin -------------------------------------------------------------------------------- /test/bin/i386/libextrahello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/libextrahello.dylib -------------------------------------------------------------------------------- /test/bin/ppc/libextrahello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/libextrahello.dylib -------------------------------------------------------------------------------- /test/bin/x86_64/hello_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/hello_expected.bin -------------------------------------------------------------------------------- /test/bin/x86_64/hello_unk_lc.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/hello_unk_lc.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/hellobundle.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/hellobundle.so -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/libhello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/libhello.dylib -------------------------------------------------------------------------------- /test/bin/ppc/libhello_expected.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/libhello_expected.dylib -------------------------------------------------------------------------------- /test/bin/x86_64/libextrahello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/libextrahello.dylib -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/hellobundle.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/hellobundle.so -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/libhello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/libhello.dylib -------------------------------------------------------------------------------- /test/bin/i386/hello_rpath_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/hello_rpath_expected.bin -------------------------------------------------------------------------------- /test/bin/i386/libhello_expected.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/i386/libhello_expected.dylib -------------------------------------------------------------------------------- /test/bin/llvm/macho-invalid-fat-header: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/llvm/macho-invalid-fat-header -------------------------------------------------------------------------------- /test/bin/llvm/macho-invalid-fat_cputype: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/llvm/macho-invalid-fat_cputype -------------------------------------------------------------------------------- /test/bin/ppc/hello_rpath_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/ppc/hello_rpath_expected.bin -------------------------------------------------------------------------------- /test/bin/x86_64/libhello_expected.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/libhello_expected.dylib -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/hello_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/hello_expected.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/libextrahello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/libextrahello.dylib -------------------------------------------------------------------------------- /test/bin/x86_64/hello_rpath_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/hello_rpath_expected.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/hello_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/hello_expected.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/libextrahello.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/libextrahello.dylib -------------------------------------------------------------------------------- /test/src/libhello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void hello(void); 4 | 5 | void hello(void) 6 | { 7 | printf("Hello, World!\n"); 8 | } 9 | -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/hello_rpath_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/hello_rpath_expected.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-ppc/libhello_expected.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-ppc/libhello_expected.dylib -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/libinconsistent.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/libinconsistent.dylib -------------------------------------------------------------------------------- /test/src/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char** argv) 4 | { 5 | printf("Hello, World!\n"); 6 | 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/hello_rpath_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/hello_rpath_expected.bin -------------------------------------------------------------------------------- /test/bin/fat-i386-x86_64/libhello_expected.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/fat-i386-x86_64/libhello_expected.dylib -------------------------------------------------------------------------------- /test/bin/x86_64/dylib_use_command-weak-delay.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Homebrew/ruby-macho/HEAD/test/bin/x86_64/dylib_use_command-weak-delay.bin -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | rules: 3 | unpinned-uses: 4 | config: 5 | policies: 6 | Homebrew/actions/*: ref-pin 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.{rb,sh}] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.github/codeql/extensions/homebrew-actions.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | extensions: 3 | - addsTo: 4 | pack: codeql/actions-all 5 | extensible: trustedActionsOwnerDataModel 6 | data: 7 | - ["Homebrew"] 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | group :test, :development do 6 | gem "benchmark-ips" 7 | gem "minitest" 8 | gem "rake" 9 | gem "rubocop" 10 | gem "simplecov-cobertura", :require => false 11 | end 12 | 13 | group :development do 14 | gem "overcommit" 15 | end 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/lib" 4 | 5 | require "bundler/gem_tasks" 6 | require "rake/testtask" 7 | require_relative "test/bench" 8 | 9 | Rake::TestTask.new do |t| 10 | t.libs << "test" 11 | end 12 | 13 | desc "Run tests" 14 | task :default => :test 15 | 16 | desc "Run benchmarks" 17 | task :bench do 18 | RubyMachOBenchmark.new.run 19 | end 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Documentation. 2 | /.yardoc/ 3 | /doc/ 4 | 5 | # Gems for redistribution. 6 | /ruby-macho-*.gem 7 | 8 | # Testing. 9 | /.bundle/ 10 | /coverage/ 11 | /vendor/bundle/ 12 | 13 | # Build results for test cases before they are selectively moved to '/test/bin'. 14 | /test/src/*/*.a 15 | /test/src/*/*.bin 16 | /test/src/*/*.dylib 17 | /test/src/*/*.o 18 | /test/src/*/*.so 19 | 20 | 21 | # IDE support 22 | .ruby-version 23 | .idea/ 24 | .vscode/ 25 | 26 | # macOS metadata file 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /test/test_kc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | BOOT_KERNEL_COLLECTION = "/System/Library/KernelCollections/BootKernelExtensions.kc" 6 | 7 | if File.exist? BOOT_KERNEL_COLLECTION 8 | class KextCollectionTests < Minitest::Test 9 | include Helpers 10 | 11 | def test_load_kc 12 | MachO.open BOOT_KERNEL_COLLECTION 13 | end 14 | 15 | def test_navigate_fileset_command 16 | macho = MachO.open BOOT_KERNEL_COLLECTION 17 | 18 | fileset_entry = macho.command(:LC_FILESET_ENTRY).first 19 | assert(fileset_entry.segment) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ruby-macho.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "#{File.dirname(__FILE__)}/lib/macho" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "ruby-macho" 7 | s.version = MachO::VERSION 8 | s.summary = "ruby-macho - Mach-O file analyzer." 9 | s.description = "A library for viewing and manipulating Mach-O files in Ruby." 10 | s.authors = ["William Woodruff"] 11 | s.email = "william@yossarian.net" 12 | s.files = Dir["LICENSE", "README.md", ".yardopts", "lib/**/*"] 13 | s.required_ruby_version = ">= 3.1" 14 | s.homepage = "https://github.com/Homebrew/ruby-macho" 15 | s.license = "MIT" 16 | s.metadata["rubygems_mfa_required"] = "true" 17 | s.metadata["funding_uri"] = "https://github.com/sponsors/Homebrew" 18 | end 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | --- 3 | version: 2 4 | updates: 5 | - package-ecosystem: bundler 6 | directories: 7 | - "/" 8 | schedule: 9 | interval: weekly 10 | day: friday 11 | time: '08:00' 12 | timezone: Etc/UTC 13 | groups: 14 | bundler: 15 | patterns: 16 | - "*" 17 | allow: 18 | - dependency-type: all 19 | cooldown: 20 | default-days: 7 21 | - package-ecosystem: github-actions 22 | directory: "/" 23 | schedule: 24 | interval: weekly 25 | day: friday 26 | time: '08:00' 27 | timezone: Etc/UTC 28 | groups: 29 | github-actions: 30 | patterns: 31 | - "*" 32 | allow: 33 | - dependency-type: all 34 | cooldown: 35 | default-days: 7 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Gem to rubygems.org 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: write 17 | id-token: write 18 | 19 | environment: rubygems.org 20 | 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 28 | with: 29 | ruby-version: ruby 30 | 31 | - run: bundle install --jobs=4 32 | 33 | # Release 34 | - uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 35 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Use this file to configure the Overcommit hooks you wish to use. This will 2 | # extend the default configuration defined in: 3 | # https://github.com/sds/overcommit/blob/master/config/default.yml 4 | # 5 | # At the topmost level of this YAML file is a key representing type of hook 6 | # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can 7 | # customize each hook, such as whether to only run it on certain files (via 8 | # `include`), whether to only display output if it fails (via `quiet`), etc. 9 | # 10 | # For a complete list of hooks, see: 11 | # https://github.com/sds/overcommit/tree/master/lib/overcommit/hook 12 | # 13 | # For a complete list of options that you can use to customize hooks, see: 14 | # https://github.com/sds/overcommit#configuration 15 | # 16 | # Uncomment the following lines to make the configuration take effect. 17 | 18 | PreCommit: 19 | RuboCop: 20 | enabled: true 21 | 22 | PostCheckout: 23 | BundleInstall: 24 | enabled: true 25 | -------------------------------------------------------------------------------- /run-tests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | die() { 4 | echo "Error: $*" >&2 5 | exit 1 6 | } 7 | 8 | ensure() { 9 | "$@" \ 10 | || die "Failed to run '$*'. Aborting." 11 | } 12 | 13 | # Make sure we're in the repository root. 14 | ensure cd "${0%/*}" 15 | 16 | # Check that Bundler is available. 17 | which bundle > /dev/null 2>&1 \ 18 | || die "Failed to find 'bundle' executable in PATH." 19 | 20 | # Setup required Gems for testing. 21 | echo ">> Checking if required Gems are installed and installing missing Gems." 22 | if ! bundle check ; then 23 | ensure bundle install --path vendor/bundle 24 | fi 25 | echo 26 | 27 | # Run rubocop. 28 | echo ">> Running style enforcement." 29 | bundle exec rubocop -DES lib/ 30 | result="$?" 31 | echo 32 | 33 | echo ">> Done: ${result}." 34 | 35 | # Run the test suite. 36 | echo ">> Running test suite." 37 | bundle exec rake test 38 | result="$?" 39 | echo 40 | 41 | # Be done and return result of test suite. 42 | echo ">> Done: ${result}." 43 | exit "${result}" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, 2016, 2017, 2018 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 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | tests: 14 | strategy: 15 | matrix: 16 | platform: ["ubuntu-latest", "macos-latest"] 17 | ruby: ["3.1", "3.2", "3.3", "3.4"] 18 | runs-on: ${{ matrix.platform }} 19 | steps: 20 | - name: Set up Git repository 21 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 22 | with: 23 | persist-credentials: false 24 | 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | bundler-cache: true 30 | 31 | - name: Run tests 32 | env: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | run: bundle exec rake 35 | 36 | - name: Run RuboCop 37 | run: bundle exec rubocop -D lib/ 38 | 39 | - name: Upload coverage results 40 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 41 | with: 42 | files: coverage/coverage.xml 43 | disable_search: true 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | -------------------------------------------------------------------------------- /test/test_open.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class MachOOpenTest < Minitest::Test 6 | include Helpers 7 | 8 | def test_nonexistent_file 9 | assert_raises ArgumentError do 10 | MachO.open("/this/is/a/file/that/cannot/possibly/exist") 11 | end 12 | end 13 | 14 | # MachO.open has slightly looser qualifications for truncation than 15 | # either MachOFile.new or FatFile.new - it just makes sure that there are 16 | # enough magic bytes to read, and lets the actual parser raise a 17 | # TruncationError later on if required. 18 | def test_truncated_file 19 | tempfile_with_data("truncated_file", "\x00\x00") do |truncated_file| 20 | assert_raises MachO::TruncatedFileError do 21 | MachO.open(truncated_file.path) 22 | end 23 | end 24 | end 25 | 26 | def test_bad_magic 27 | tempfile_with_data("junk_file", "\xFF\xFF\xFF\xFF") do |junk_file| 28 | assert_raises MachO::MagicError do 29 | MachO.open(junk_file.path) 30 | end 31 | end 32 | end 33 | 34 | def test_open 35 | file = MachO.open(fixture(:x86_64, "libhello.dylib")) 36 | 37 | assert_kind_of MachO::MachOFile, file 38 | 39 | file = MachO.open(fixture(%i[i386 x86_64], "libhello.dylib")) 40 | 41 | assert_kind_of MachO::FatFile, file 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/src/make-inconsistent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | main() { 4 | local use_dir 5 | 6 | for use_dir in "${@}"; do 7 | # we only want to build libinconsistent.dylib as a fat mach-o 8 | [[ "${use_dir}" = fat* ]] || continue 9 | inconsistent_for "${use_dir}" 10 | done 11 | } 12 | 13 | inconsistent_for() { 14 | # splits a fat directory spec like fat-i386-x86_64 into 15 | # its constituent arch(3) pairs (e.g., i386 and x86_64) 16 | local fat_dir="${1}" 17 | local split_fat_dir 18 | IFS=- read -a split_fat_dir <<<"${fat_dir#fat-}" 19 | 20 | # future versions of the test suite might have more than two architectures 21 | # in a fat file, but we only care about the first two here 22 | local arch1="${split_fat_dir[0]}" 23 | local arch2="${split_fat_dir[1]}" 24 | 25 | # order is arbitrary, as long as the libs chosen have different linkages 26 | local lib1="${arch1}/libhello.dylib" 27 | local lib2="${arch2}/libextrahello.dylib" 28 | 29 | [[ -f "${lib1}" ]] || die "Missing file: ${lib1}. Did you run make?" 30 | [[ -f "${lib2}" ]] || die "Missing file: ${lib2}. Did you run make?" 31 | 32 | echo "[+] Creating libinconsistent.dylib for ${fat_dir}" 33 | lipo -create "${lib1}" "${lib2}" -output "${fat_dir}/libinconsistent.dylib" 34 | } 35 | 36 | die() { 37 | echo "Error: ${*}" >&2 38 | exit 1 39 | } 40 | 41 | main "${@}" 42 | -------------------------------------------------------------------------------- /lib/macho/view.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # A representation of some unspecified Mach-O data. 5 | class MachOView 6 | # @return [MachOFile] that this view belongs to 7 | attr_reader :macho_file 8 | 9 | # @return [String] the raw Mach-O data 10 | attr_reader :raw_data 11 | 12 | # @return [Symbol] the endianness of the data (`:big` or `:little`) 13 | attr_reader :endianness 14 | 15 | # @return [Integer] the offset of the relevant data (in {#raw_data}) 16 | attr_reader :offset 17 | 18 | # Creates a new MachOView. 19 | # @param macho_file [MachOFile] the file this view slice is from 20 | # @param raw_data [String] the raw Mach-O data 21 | # @param endianness [Symbol] the endianness of the data 22 | # @param offset [Integer] the offset of the relevant data 23 | def initialize(macho_file, raw_data, endianness, offset) 24 | @macho_file = macho_file 25 | @raw_data = raw_data 26 | @endianness = endianness 27 | @offset = offset 28 | end 29 | 30 | # @return [Hash] a hash representation of this {MachOView}. 31 | def to_h 32 | { 33 | "endianness" => endianness, 34 | "offset" => offset, 35 | } 36 | end 37 | 38 | def inspect 39 | "#<#{self.class}:0x#{(object_id << 1).to_s(16)} @endianness=#{@endianness.inspect}, @offset=#{@offset.inspect}, length=#{@raw_data.length}>" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 3.1 3 | NewCops: enable 4 | 5 | Style/StringLiterals: 6 | EnforcedStyle: double_quotes 7 | 8 | Style/StringLiteralsInInterpolation: 9 | EnforcedStyle: double_quotes 10 | 11 | Style/TrailingCommaInArrayLiteral: 12 | EnforcedStyleForMultiline: comma 13 | 14 | Style/TrailingCommaInHashLiteral: 15 | EnforcedStyleForMultiline: comma 16 | 17 | Style/FormatString: 18 | EnforcedStyle: percent 19 | 20 | # we prefer compact if-else-end/case-when-end alignment 21 | Layout/EndAlignment: 22 | EnforcedStyleAlignWith: variable 23 | 24 | Style/HashSyntax: 25 | EnforcedStyle: hash_rockets 26 | 27 | Style/TrailingCommaInArguments: 28 | EnforcedStyleForMultiline: no_comma 29 | 30 | Layout/CaseIndentation: 31 | EnforcedStyle: end 32 | 33 | # TODO: re-enable in the future 34 | Layout/LineLength: 35 | Enabled: false 36 | 37 | Metrics/AbcSize: 38 | Enabled: false 39 | 40 | Metrics/ClassLength: 41 | Enabled: false 42 | 43 | Metrics/CyclomaticComplexity: 44 | Enabled: false 45 | 46 | Metrics/MethodLength: 47 | Enabled: false 48 | 49 | Metrics/ModuleLength: 50 | Enabled: false 51 | 52 | Metrics/ParameterLists: 53 | Enabled: false 54 | 55 | Metrics/PerceivedComplexity: 56 | Enabled: false 57 | 58 | Naming/MethodParameterName: 59 | Enabled: false 60 | 61 | Naming/VariableNumber: 62 | CheckSymbols: false 63 | Exclude: 64 | - 'test/**' 65 | 66 | Metrics/BlockLength: 67 | Exclude: 68 | - 'test/**' 69 | 70 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.3) 5 | benchmark-ips (2.14.0) 6 | childprocess (5.1.0) 7 | logger (~> 1.5) 8 | docile (1.4.1) 9 | iniparse (1.5.0) 10 | json (2.18.0) 11 | language_server-protocol (3.17.0.5) 12 | lint_roller (1.1.0) 13 | logger (1.7.0) 14 | minitest (5.26.2) 15 | overcommit (0.68.0) 16 | childprocess (>= 0.6.3, < 6) 17 | iniparse (~> 1.4) 18 | rexml (>= 3.3.9) 19 | parallel (1.27.0) 20 | parser (3.3.10.0) 21 | ast (~> 2.4.1) 22 | racc 23 | prism (1.6.0) 24 | racc (1.8.1) 25 | rainbow (3.1.1) 26 | rake (13.3.1) 27 | regexp_parser (2.11.3) 28 | rexml (3.4.4) 29 | rubocop (1.81.7) 30 | json (~> 2.3) 31 | language_server-protocol (~> 3.17.0.2) 32 | lint_roller (~> 1.1.0) 33 | parallel (~> 1.10) 34 | parser (>= 3.3.0.2) 35 | rainbow (>= 2.2.2, < 4.0) 36 | regexp_parser (>= 2.9.3, < 3.0) 37 | rubocop-ast (>= 1.47.1, < 2.0) 38 | ruby-progressbar (~> 1.7) 39 | unicode-display_width (>= 2.4.0, < 4.0) 40 | rubocop-ast (1.48.0) 41 | parser (>= 3.3.7.2) 42 | prism (~> 1.4) 43 | ruby-progressbar (1.13.0) 44 | simplecov (0.22.0) 45 | docile (~> 1.1) 46 | simplecov-html (~> 0.11) 47 | simplecov_json_formatter (~> 0.1) 48 | simplecov-cobertura (3.1.0) 49 | rexml 50 | simplecov (~> 0.19) 51 | simplecov-html (0.13.2) 52 | simplecov_json_formatter (0.1.4) 53 | unicode-display_width (3.2.0) 54 | unicode-emoji (~> 4.1) 55 | unicode-emoji (4.1.0) 56 | 57 | PLATFORMS 58 | ruby 59 | 60 | DEPENDENCIES 61 | benchmark-ips 62 | minitest 63 | overcommit 64 | rake 65 | rubocop 66 | simplecov-cobertura 67 | 68 | BUNDLED WITH 69 | 2.3.5 70 | -------------------------------------------------------------------------------- /test/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # if we're running the tests on the CI, generate a coverage report 4 | if ENV["CI"] 5 | require "simplecov" 6 | require "simplecov-cobertura" 7 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 8 | SimpleCov.start 9 | end 10 | 11 | require "digest/sha1" 12 | require "fileutils" 13 | require "minitest/autorun" 14 | require "tempfile" 15 | 16 | require "macho" 17 | 18 | module Helpers 19 | OTOOL_RX = /\t(.*) \(compatibility version (?:\d+\.)*\d+, current version (?:\d+\.)*\d+\)/.freeze 20 | 21 | # architectures used in testing 32-bit single-arch binaries 22 | SINGLE_32_ARCHES = %i[ 23 | i386 24 | ppc 25 | ].freeze 26 | 27 | # architectures used in testing 64-bit single-arch binaries 28 | SINGLE_64_ARCHES = [ 29 | :x86_64, 30 | ].freeze 31 | 32 | # architectures used in testing single-arch binaries 33 | SINGLE_ARCHES = SINGLE_32_ARCHES + SINGLE_64_ARCHES 34 | 35 | # architecture pairs used in testing fat binaries 36 | FAT_ARCH_PAIRS = [ 37 | %i[i386 x86_64], 38 | %i[i386 ppc], 39 | ].freeze 40 | 41 | def fixture(archs, name) 42 | arch_dir = archs.is_a?(Array) ? "fat-#{archs.join("-")}" : archs.to_s 43 | "test/bin/#{arch_dir}/#{name}" 44 | end 45 | 46 | def installed?(util) 47 | !`which #{util}`.empty? 48 | end 49 | 50 | def delete_if_exists(file) 51 | FileUtils.rm_f(file) 52 | end 53 | 54 | def equal_sha1_hashes(file1, file2) 55 | digest1 = Digest::SHA1.file(file1).to_s 56 | digest2 = Digest::SHA1.file(file2).to_s 57 | 58 | digest1 == digest2 59 | end 60 | 61 | def filechecks(except = nil) 62 | checks = %i[ 63 | object? executable? fvmlib? core? preload? dylib? 64 | dylinker? bundle? dsym? kext? 65 | ] 66 | 67 | checks.delete(except) 68 | 69 | checks 70 | end 71 | 72 | def tempfile_with_data(filename, data) 73 | Tempfile.open(filename) do |file| 74 | file.write(data) 75 | file.rewind 76 | yield file 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/macho.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "open3" 4 | 5 | require_relative "macho/utils" 6 | require_relative "macho/structure" 7 | require_relative "macho/view" 8 | require_relative "macho/headers" 9 | require_relative "macho/load_commands" 10 | require_relative "macho/sections" 11 | require_relative "macho/macho_file" 12 | require_relative "macho/fat_file" 13 | require_relative "macho/exceptions" 14 | require_relative "macho/tools" 15 | 16 | # The primary namespace for ruby-macho. 17 | module MachO 18 | # release version 19 | VERSION = "4.1.0" 20 | 21 | # Opens the given filename as a MachOFile or FatFile, depending on its magic. 22 | # @param filename [String] the file being opened 23 | # @return [MachOFile] if the file is a Mach-O 24 | # @return [FatFile] if the file is a Fat file 25 | # @raise [ArgumentError] if the given file does not exist 26 | # @raise [TruncatedFileError] if the file is too small to have a valid header 27 | # @raise [MagicError] if the file's magic is not valid Mach-O magic 28 | def self.open(filename) 29 | raise ArgumentError, "#{filename}: no such file" unless File.file?(filename) 30 | raise TruncatedFileError unless File.stat(filename).size >= 4 31 | 32 | magic = File.open(filename, "rb") { |f| f.read(4) }.unpack1("N") 33 | 34 | if Utils.fat_magic?(magic) 35 | file = FatFile.new(filename) 36 | elsif Utils.magic?(magic) 37 | file = MachOFile.new(filename) 38 | else 39 | raise MagicError, magic 40 | end 41 | 42 | file 43 | end 44 | 45 | # Signs the dylib using an ad-hoc identity. 46 | # Necessary after making any changes to a dylib, since otherwise 47 | # changing a signed file invalidates its signature. 48 | # @param filename [String] the file being opened 49 | # @return [void] 50 | # @raise [ModificationError] if the operation fails 51 | def self.codesign!(filename) 52 | raise ArgumentError, "codesign binary is not available on Linux" if RUBY_PLATFORM !~ /darwin/ 53 | raise ArgumentError, "#{filename}: no such file" unless File.file?(filename) 54 | 55 | _, _, status = Open3.capture3("codesign", "--sign", "-", "--force", 56 | "--preserve-metadata=entitlements,requirements,flags,runtime", 57 | filename) 58 | 59 | raise CodeSigningError, "#{filename}: signing failed!" unless status.success? 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/test_create_load_commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class MachOLoadCommandCreationTest < Minitest::Test 6 | include Helpers 7 | 8 | def test_create_uncreatable_command 9 | assert_raises MachO::LoadCommandNotCreatableError do 10 | MachO::LoadCommands::LoadCommand.create(:LC_SEGMENT, 4) 11 | end 12 | end 13 | 14 | def test_create_wrong_command_arity 15 | assert_raises MachO::LoadCommandCreationArityError do 16 | MachO::LoadCommands::LoadCommand.create(:LC_ID_DYLIB, 4, "missing arguments") 17 | end 18 | end 19 | 20 | def test_create_dylib_commands 21 | # all dylib commands are creatable, so test them all 22 | dylib_commands = MachO::LoadCommands::DYLIB_LOAD_COMMANDS + [:LC_ID_DYLIB] 23 | dylib_commands.each do |cmd_sym| 24 | lc = MachO::LoadCommands::LoadCommand.create(cmd_sym, "test", 0, 0, 0) 25 | 26 | assert lc 27 | assert_instance_of MachO::LoadCommands::DylibCommand, lc 28 | assert lc.name 29 | assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.name 30 | assert_equal "test", lc.name.to_s 31 | assert_equal lc.name.to_s, lc.to_s 32 | assert_equal 0, lc.timestamp 33 | assert_equal 0, lc.current_version 34 | assert_equal 0, lc.compatibility_version 35 | assert_instance_of String, lc.view.inspect 36 | end 37 | end 38 | 39 | def test_create_dylib_commands_new 40 | # all dylib commands are creatable, so test them all 41 | dylib_commands = %i[LC_LOAD_DYLIB LC_LOAD_WEAK_DYLIB] 42 | dylib_commands.each do |cmd_sym| 43 | lc = MachO::LoadCommands::LoadCommand.create(cmd_sym, "test", MachO::LoadCommands::DYLIB_USE_MARKER, 0, 0, 0) 44 | 45 | assert lc 46 | assert_instance_of MachO::LoadCommands::DylibUseCommand, lc 47 | assert lc.name 48 | assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.name 49 | assert_equal "test", lc.name.to_s 50 | assert_equal lc.name.to_s, lc.to_s 51 | assert_equal MachO::LoadCommands::DYLIB_USE_MARKER, lc.timestamp 52 | assert_equal 0, lc.current_version 53 | assert_equal 0, lc.compatibility_version 54 | assert_equal 0, lc.flags 55 | assert_instance_of String, lc.view.inspect 56 | end 57 | end 58 | 59 | def test_create_rpath_command 60 | lc = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, "test") 61 | 62 | assert lc 63 | assert_kind_of MachO::LoadCommands::RpathCommand, lc 64 | assert lc.path 65 | assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.path 66 | assert_equal "test", lc.path.to_s 67 | assert_equal lc.path.to_s, lc.to_s 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruby-macho 2 | ================ 3 | 4 | [![Gem Version](https://badge.fury.io/rb/ruby-macho.svg)](http://badge.fury.io/rb/ruby-macho) 5 | [![CI](https://github.com/Homebrew/ruby-macho/actions/workflows/tests.yml/badge.svg)](https://github.com/Homebrew/ruby-macho/actions/workflows/tests.yml) 6 | [![Coverage Status](https://codecov.io/gh/Homebrew/ruby-macho/branch/main/graph/badge.svg)](https://codecov.io/gh/Homebrew/ruby-macho) 7 | 8 | A Ruby library for examining and modifying Mach-O files. 9 | 10 | ### What is a Mach-O file? 11 | 12 | The [Mach-O file format](https://en.wikipedia.org/wiki/Mach-O) is used by macOS 13 | and iOS (among others) as a general purpose binary format for object files, 14 | executables, dynamic libraries, and so forth. 15 | 16 | ### Installation 17 | 18 | ruby-macho can be installed via RubyGems: 19 | 20 | ```bash 21 | $ gem install ruby-macho 22 | ``` 23 | 24 | ### Documentation 25 | 26 | Full documentation is available on [RubyDoc](http://www.rubydoc.info/gems/ruby-macho/). 27 | 28 | A quick example of what ruby-macho can do: 29 | 30 | ```ruby 31 | require 'macho' 32 | 33 | file = MachO::MachOFile.new("/path/to/my/binary") 34 | 35 | # get the file's type (object, dynamic lib, executable, etc) 36 | file.filetype # => :execute 37 | 38 | # get all load commands in the file and print their offsets: 39 | file.load_commands.each do |lc| 40 | puts "#{lc.type}: offset #{lc.offset}, size: #{lc.cmdsize}" 41 | end 42 | 43 | # access a specific load command 44 | lc_vers = file[:LC_VERSION_MIN_MACOSX].first 45 | puts lc_vers.version_string # => "10.10.0" 46 | ``` 47 | 48 | ### What works? 49 | 50 | * Reading data from x86/x86_64/PPC Mach-O files 51 | * Changing the IDs of Mach-O and Fat dylibs 52 | * Changing install names in Mach-O and Fat files 53 | * Adding, deleting, and modifying rpaths. 54 | 55 | ### What needs to be done? 56 | 57 | * Unit and performance testing. 58 | 59 | ### Contributing, setting up `overcommit` and the linters 60 | 61 | In order to keep the repo, docs and data tidy, we use a tool called [`overcommit`](https://github.com/sds/overcommit) 62 | to connect up the git hooks to a set of quality checks. The fastest way to get setup is to run the following to make 63 | sure you have all the tools: 64 | 65 | ```shell 66 | gem install overcommit bundler 67 | bundle install 68 | overcommit --install 69 | ``` 70 | 71 | ### Attribution 72 | 73 | * Constants were taken from Apple, Inc's 74 | [`loader.h` in `cctools/include/mach-o`](https://opensource.apple.com/source/cctools/cctools-973.0.1/include/mach-o/loader.h.auto.html). 75 | (Apple Public Source License 2.0). 76 | * Binary files used for testing were taken from The LLVM Project. ([Apache License v2.0 with LLVM Exceptions](test/bin/llvm/LICENSE.txt)). 77 | 78 | ### License 79 | 80 | `ruby-macho` is licensed under the MIT License. 81 | 82 | For the exact terms, see the [license](LICENSE) file. 83 | -------------------------------------------------------------------------------- /machostructure-dsl-docs.md: -------------------------------------------------------------------------------- 1 | # Internal MachOStructure DSL 2 | ## Documentation 3 | The MachOStructure class makes it easy to describe binary chunks by using the #field method. This method generates the byte size and format strings necessary to parse a chunk of binary data. It also automatically generates the constructor and readers for all fields as well. 4 | 5 | The fields are created in order so you will be expected to pass those arguments to the constructor in the same order. Fields with no arguments should be defined last and fields with default arguments should be defined right before them. 6 | 7 | The type and options of inherited fields can be changed but their argument position and the number of arguments (used to calculate min_args) will also not change. 8 | 9 | Usually, endianness is handled by the Utils#specialize_format method but occasionally a field needs to specify that beforehand. That is what the :endian option is for. If not specified, a placeholder is used so that can be specified later. 10 | 11 | ## Syntax 12 | ```ruby 13 | field [field name], [field type], [option1 => value1], [option2 => value2], ... 14 | ``` 15 | 16 | ## Example 17 | ```ruby 18 | class AllFields < MachO::MachOStructure 19 | field :name1, :string, :size => 16 20 | field :name3, :int32 21 | field :name4, :uint32 22 | field :name5, :uint64 23 | field :name6, :view 24 | field :name7, :lcstr 25 | field :name8, :two_level_hints_table 26 | field :name9, :tool_entries 27 | end 28 | ``` 29 | 30 | ## Field Types 31 | - `:string` [requires `:size` option] [optional `:padding` option] 32 | - a string 33 | - `:int32 ` 34 | - a signed 32 bit integer 35 | - `:uint32 ` 36 | - an unsigned 32 bit integer 37 | - `:uint64 ` 38 | - an unsigned 64 bit integer 39 | - `:view` [initialized] 40 | - an instance of the MachOView class (lib/macho/view.rb) 41 | - `:lcstr` [NOT initialized] 42 | - an instance of the LCStr class (lib/macho/load_commands.rb) 43 | - `:two_level_hints_table` [NOT initialized] [NO argument] 44 | - an instance of the TwoLevelHintsTable class (lib/macho/load_commands.rb) 45 | - `:tool_entries` [NOT initialized] 46 | - an instance of the ToolEntries class (lib/macho/load_commands.rb) 47 | 48 | ## Option Types 49 | - Exclusive (only one can be used at a time) 50 | - `:mask` [Integer] bitmask to be applied to field 51 | - `:unpack` [String] binary unpack string used for further unpacking of :string 52 | - `:default` [Value] default field value 53 | - Inclusive (can be used with other options) 54 | - `:to_s` [Boolean] generate `#to_s` method based on field 55 | - Used with Integer field types 56 | - `:endian` [Symbol] optionally specify `:big` or `:little` endian 57 | - Used with `:string` field type 58 | - `:size` [Integer] size in bytes 59 | - `:padding` [Symbol] optionally specify `:null` padding 60 | 61 | ## More Information 62 | Hop over to lib/macho/structure.rb to see the class itself. 63 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | name: Manage stale issues 3 | 4 | on: 5 | push: 6 | paths: 7 | - .github/workflows/stale-issues.yml 8 | branches-ignore: 9 | - dependabot/** 10 | schedule: 11 | # Once every day at midnight UTC 12 | - cron: "0 0 * * *" 13 | issue_comment: 14 | 15 | permissions: {} 16 | 17 | defaults: 18 | run: 19 | shell: bash -xeuo pipefail {0} 20 | 21 | concurrency: 22 | group: stale-issues 23 | cancel-in-progress: ${{ github.event_name != 'issue_comment' }} 24 | 25 | jobs: 26 | stale: 27 | if: > 28 | github.repository_owner == 'Homebrew' && ( 29 | github.event_name != 'issue_comment' || ( 30 | contains(github.event.issue.labels.*.name, 'stale') || 31 | contains(github.event.pull_request.labels.*.name, 'stale') 32 | ) 33 | ) 34 | runs-on: ubuntu-latest 35 | permissions: 36 | contents: write 37 | issues: write 38 | pull-requests: write 39 | steps: 40 | - name: Mark/Close Stale Issues and Pull Requests 41 | uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 42 | with: 43 | repo-token: ${{ secrets.GITHUB_TOKEN }} 44 | days-before-stale: 21 45 | days-before-close: 7 46 | stale-issue-message: > 47 | This issue has been automatically marked as stale because it has not had 48 | recent activity. It will be closed if no further activity occurs. 49 | stale-pr-message: > 50 | This pull request has been automatically marked as stale because it has not had 51 | recent activity. It will be closed if no further activity occurs. 52 | exempt-issue-labels: "gsoc-outreachy,help wanted,in progress" 53 | exempt-pr-labels: "gsoc-outreachy,help wanted,in progress" 54 | delete-branch: true 55 | 56 | bump-pr-stale: 57 | if: > 58 | github.repository_owner == 'Homebrew' && ( 59 | github.event_name != 'issue_comment' || ( 60 | contains(github.event.issue.labels.*.name, 'stale') || 61 | contains(github.event.pull_request.labels.*.name, 'stale') 62 | ) 63 | ) 64 | runs-on: ubuntu-latest 65 | permissions: 66 | contents: write 67 | issues: write 68 | pull-requests: write 69 | steps: 70 | - name: Mark/Close Stale `bump-formula-pr` and `bump-cask-pr` Pull Requests 71 | uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 72 | with: 73 | repo-token: ${{ secrets.GITHUB_TOKEN }} 74 | days-before-stale: 2 75 | days-before-close: 1 76 | stale-pr-message: > 77 | This pull request has been automatically marked as stale because it has not had 78 | recent activity. It will be closed if no further activity occurs. To keep this 79 | pull request open, add a `help wanted` or `in progress` label. 80 | exempt-pr-labels: "help wanted,in progress" 81 | any-of-labels: "bump-formula-pr,bump-cask-pr" 82 | delete-branch: true 83 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | name: Actionlint 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | 11 | defaults: 12 | run: 13 | shell: bash -xeuo pipefail {0} 14 | 15 | concurrency: 16 | group: "actionlint-${{ github.ref }}" 17 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 18 | 19 | env: 20 | HOMEBREW_DEVELOPER: 1 21 | HOMEBREW_NO_AUTO_UPDATE: 1 22 | HOMEBREW_NO_ENV_HINTS: 1 23 | 24 | permissions: {} 25 | 26 | jobs: 27 | workflow_syntax: 28 | if: github.repository_owner == 'Homebrew' 29 | runs-on: ubuntu-latest 30 | permissions: 31 | contents: read 32 | container: 33 | image: ghcr.io/homebrew/ubuntu22.04:main 34 | steps: 35 | - name: Set up Homebrew 36 | id: setup-homebrew 37 | uses: Homebrew/actions/setup-homebrew@main 38 | with: 39 | core: false 40 | cask: false 41 | 42 | - name: Install tools 43 | run: brew install actionlint shellcheck zizmor 44 | 45 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 46 | with: 47 | persist-credentials: false 48 | 49 | - run: zizmor --format sarif . > results.sarif 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Upload SARIF file 54 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 55 | # We can't use the SARIF file when triggered by `merge_group` so we don't upload it. 56 | if: always() && github.event_name != 'merge_group' 57 | with: 58 | name: results.sarif 59 | path: results.sarif 60 | 61 | - name: Set up actionlint 62 | run: | 63 | # In homebrew-core, setting `shell: /bin/bash` prevents shellcheck from running on 64 | # those steps, so let's change them to `shell: bash` temporarily for better linting. 65 | sed -i 's|shell: /bin/bash -x|shell: bash -x|' .github/workflows/*.y*ml 66 | 67 | # In homebrew-core, the JSON matcher needs to be accessible to the container host. 68 | cp "$(brew --repository)/.github/actionlint-matcher.json" "$HOME" 69 | 70 | echo "::add-matcher::$HOME/actionlint-matcher.json" 71 | 72 | - run: actionlint 73 | 74 | upload_sarif: 75 | needs: workflow_syntax 76 | # We want to always upload this even if `actionlint` failed. 77 | # This is only available on public repositories. 78 | if: > 79 | always() && 80 | !contains(fromJSON('["cancelled", "skipped"]'), needs.workflow_syntax.result) && 81 | !github.event.repository.private && 82 | github.event_name != 'merge_group' 83 | runs-on: ubuntu-latest 84 | permissions: 85 | contents: read 86 | security-events: write 87 | steps: 88 | - name: Download SARIF file 89 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 90 | with: 91 | name: results.sarif 92 | path: results.sarif 93 | 94 | - name: Upload SARIF file 95 | uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 96 | with: 97 | sarif_file: results.sarif 98 | category: zizmor 99 | -------------------------------------------------------------------------------- /test/test_structure_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class MachOStructureTest < Minitest::Test 6 | # Test that every field type can be created and that 7 | # that information is reflected in the bytesize, min_args 8 | # and format. 9 | class AllFields < MachO::MachOStructure 10 | field :string, :string, :size => 16 11 | field :null_term_str, :string, :padding => :null, :size => 32 12 | field :int32, :int32 13 | field :uint32, :uint32 14 | field :uint64, :uint64 15 | field :view, :view 16 | field :lcstr, :lcstr 17 | field :two_level_hints_table, :two_level_hints_table 18 | field :tool_entries, :tool_entries 19 | end 20 | 21 | def test_all_field_types 22 | assert_includes AllFields.instance_methods, :string 23 | assert_includes AllFields.instance_methods, :null_term_str 24 | assert_includes AllFields.instance_methods, :int32 25 | assert_includes AllFields.instance_methods, :uint32 26 | assert_includes AllFields.instance_methods, :uint64 27 | assert_includes AllFields.instance_methods, :view 28 | assert_includes AllFields.instance_methods, :lcstr 29 | assert_includes AllFields.instance_methods, :two_level_hints_table 30 | assert_includes AllFields.instance_methods, :tool_entries 31 | 32 | assert_equal AllFields.bytesize, 72 33 | assert_equal AllFields.format, "a16Z32l=L=Q=L=L=" 34 | assert_equal AllFields.min_args, 8 35 | end 36 | 37 | # Test that fields already defined in the base class 38 | # are updated correctly when redefined in the 39 | # derived class. 40 | class BaseCmd < MachO::MachOStructure 41 | field :field1, :uint32 42 | field :field2, :uint32 43 | end 44 | 45 | class DerivedCmd < BaseCmd 46 | field :field1, :uint64 47 | field :field2, :uint64 48 | end 49 | 50 | def test_updating_fields 51 | assert_equal BaseCmd.bytesize, 8 52 | assert_equal BaseCmd.format, "L=L=" 53 | assert_equal DerivedCmd.bytesize, 16 54 | assert_equal DerivedCmd.format, "Q=Q=" 55 | end 56 | 57 | # Tests that make sure that all of the options work 58 | # correctly (except for :size which is already tested above). 59 | class MaskCmd < MachO::MachOStructure 60 | field :mask_field, :uint32, :mask => 0xffff0000 61 | end 62 | 63 | def test_mask_option 64 | mask_struct = MaskCmd.new(0xffffffff) 65 | assert_equal mask_struct.mask_field, 0x0000ffff 66 | end 67 | 68 | class UnpackCmd < MachO::MachOStructure 69 | field :unpack_field, :string, :size => 8, :unpack => "L>2" 70 | end 71 | 72 | def test_unpack_option 73 | numbers = [42, 1337].freeze 74 | format_code = "L>2" 75 | packed_numbers = numbers.pack(format_code) 76 | 77 | unpack_struct = UnpackCmd.new(packed_numbers) 78 | assert_equal unpack_struct.unpack_field, numbers 79 | end 80 | 81 | class DefaultCmd < MachO::MachOStructure 82 | field :default_field, :uint64, :default => 0 83 | end 84 | 85 | def test_default_option 86 | default_struct = DefaultCmd.new 87 | assert_equal default_struct.default_field, 0 88 | default_struct = DefaultCmd.new(4) 89 | assert_equal default_struct.default_field, 4 90 | end 91 | 92 | class StringCmd < MachO::MachOStructure 93 | field :uint32, :uint32, :to_s => true 94 | end 95 | 96 | def test_to_s_option 97 | string_cmd = StringCmd.new(10) 98 | assert_equal string_cmd.to_s, "10" 99 | end 100 | 101 | class EndianCmd < MachO::MachOStructure 102 | field :uint32_big, :uint32, :endian => :big 103 | field :uint32_little, :uint32, :endian => :little 104 | end 105 | 106 | def test_endian_option 107 | assert_equal EndianCmd.format, "L>L<" 108 | end 109 | 110 | class PaddingCmd < MachO::MachOStructure 111 | field :str, :string, :size => 12 112 | field :null_term_str, :string, :padding => :null, :size => 12 113 | end 114 | 115 | def test_padding_option 116 | assert_equal PaddingCmd.format, "a12Z12" 117 | assert_equal PaddingCmd.bytesize, 24 118 | 119 | padded_str = "Hello\x00World!" * 2 120 | padding_cmd = PaddingCmd.new_from_bin(:big, padded_str) 121 | assert_equal padding_cmd.str, "Hello\x00World!" 122 | assert_equal padding_cmd.null_term_str, "Hello" 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/macho/tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # A collection of convenient methods for common operations on Mach-O and Fat 5 | # binaries. 6 | module Tools 7 | # @param filename [String] the Mach-O or Fat binary being read 8 | # @return [Array] an array of all dylibs linked to the binary 9 | def self.dylibs(filename) 10 | file = MachO.open(filename) 11 | 12 | file.linked_dylibs 13 | end 14 | 15 | # Changes the dylib ID of a Mach-O or Fat binary, overwriting the source 16 | # file. 17 | # @param filename [String] the Mach-O or Fat binary being modified 18 | # @param new_id [String] the new dylib ID for the binary 19 | # @param options [Hash] 20 | # @option options [Boolean] :strict (true) whether or not to fail loudly 21 | # with an exception if the change cannot be performed 22 | # @return [void] 23 | def self.change_dylib_id(filename, new_id, options = {}) 24 | file = MachO.open(filename) 25 | 26 | file.change_dylib_id(new_id, options) 27 | file.write! 28 | end 29 | 30 | # Changes a shared library install name in a Mach-O or Fat binary, 31 | # overwriting the source file. 32 | # @param filename [String] the Mach-O or Fat binary being modified 33 | # @param old_name [String] the old shared library name 34 | # @param new_name [String] the new shared library name 35 | # @param options [Hash] 36 | # @option options [Boolean] :strict (true) whether or not to fail loudly 37 | # with an exception if the change cannot be performed 38 | # @return [void] 39 | def self.change_install_name(filename, old_name, new_name, options = {}) 40 | file = MachO.open(filename) 41 | 42 | file.change_install_name(old_name, new_name, options) 43 | file.write! 44 | end 45 | 46 | # Changes a runtime path in a Mach-O or Fat binary, overwriting the source 47 | # file. 48 | # @param filename [String] the Mach-O or Fat binary being modified 49 | # @param old_path [String] the old runtime path 50 | # @param new_path [String] the new runtime path 51 | # @param options [Hash] 52 | # @option options [Boolean] :strict (true) whether or not to fail loudly 53 | # with an exception if the change cannot be performed 54 | # @option options [Boolean] :uniq (false) whether or not to change duplicate 55 | # rpaths simultaneously 56 | # @return [void] 57 | def self.change_rpath(filename, old_path, new_path, options = {}) 58 | file = MachO.open(filename) 59 | 60 | file.change_rpath(old_path, new_path, options) 61 | file.write! 62 | end 63 | 64 | # Add a runtime path to a Mach-O or Fat binary, overwriting the source file. 65 | # @param filename [String] the Mach-O or Fat binary being modified 66 | # @param new_path [String] the new runtime path 67 | # @param options [Hash] 68 | # @option options [Boolean] :strict (true) whether or not to fail loudly 69 | # with an exception if the change cannot be performed 70 | # @return [void] 71 | def self.add_rpath(filename, new_path, options = {}) 72 | file = MachO.open(filename) 73 | 74 | file.add_rpath(new_path, options) 75 | file.write! 76 | end 77 | 78 | # Delete a runtime path from a Mach-O or Fat binary, overwriting the source 79 | # file. 80 | # @param filename [String] the Mach-O or Fat binary being modified 81 | # @param old_path [String] the old runtime path 82 | # @param options [Hash] 83 | # @option options [Boolean] :strict (true) whether or not to fail loudly 84 | # with an exception if the change cannot be performed 85 | # @option options [Boolean] :uniq (false) whether or not to delete duplicate 86 | # rpaths simultaneously 87 | # @return [void] 88 | def self.delete_rpath(filename, old_path, options = {}) 89 | file = MachO.open(filename) 90 | 91 | file.delete_rpath(old_path, options) 92 | file.write! 93 | end 94 | 95 | # Merge multiple Mach-Os into one universal (Fat) binary. 96 | # @param filename [String] the fat binary to create 97 | # @param files [Array] the files to merge 98 | # @param fat64 [Boolean] whether to use {Headers::FatArch64}s to represent each slice 99 | # @return [void] 100 | def self.merge_machos(filename, *files, fat64: false) 101 | machos = files.map do |file| 102 | macho = MachO.open(file) 103 | case macho 104 | when MachO::MachOFile 105 | macho 106 | else 107 | macho.machos 108 | end 109 | end.flatten 110 | 111 | fat_macho = MachO::FatFile.new_from_machos(*machos, :fat64 => fat64) 112 | fat_macho.write(filename) 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/src/Makefile: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # make USE=10.6-xcode3.2.6 3 | # make USE=10.11-xcode7.3 4 | # make USE=15-xcode16.0 5 | 6 | HELLO_SRC = hello.c 7 | LIBHELLO_SRC = libhello.c 8 | RPATH_FLAGS = -Wl,-rpath,made_up_path 9 | TARGET_FILES = \ 10 | hello.o \ 11 | hello.bin \ 12 | hello_expected.bin \ 13 | hello_rpath_expected.bin \ 14 | libhello.dylib \ 15 | libhello_expected.dylib \ 16 | libextrahello.dylib \ 17 | hellobundle.so 18 | 19 | # Architecture-specific flags that are set by pattern matching the targets. 20 | ARCH_FLAGS = 21 | i386/% : ARCH_FLAGS = -arch i386 22 | x86_64/% : ARCH_FLAGS = -arch x86_64 23 | ppc/% : ARCH_FLAGS = -arch ppc 24 | ppc64/% : ARCH_FLAGS = -arch ppc64 25 | fat-i386-x86_64/% : ARCH_FLAGS = -arch i386 -arch x86_64 26 | fat-i386-ppc/% : ARCH_FLAGS = -arch i386 -arch ppc 27 | fat-ppc-ppc64/% : ARCH_FLAGS = -arch ppc -arch ppc64 28 | 29 | # Make sure we're on OS X. 30 | UNAME_S := $(shell uname -s) 31 | ifneq ($(UNAME_S),Darwin) 32 | $(error This makefile can only be run on OS X, but detected $(UNAME_S)) 33 | endif 34 | 35 | # Select one of the pre-defined subsets (depends on host OS X and Xcode). 36 | ALL_DIRS := i386 x86_64 ppc ppc64 fat-i386-x86_64 fat-i386-ppc fat-ppc-ppc64 37 | ifneq ($(USE_DIRS),) 38 | # Trust the user to get this right, if explicitly specified. 39 | else ifeq ($(USE),all) 40 | $(warning USE - Using 'all' is unlikely to work on a single host.) 41 | USE_DIRS := $(ALL_DIRS) 42 | else ifeq ($(USE),10.6-xcode3.2.6) 43 | USE_DIRS := i386 x86_64 ppc fat-i386-x86_64 fat-i386-ppc 44 | NO_UPWARD := 1 45 | NO_LAZY := 1 46 | NO_DELAY_INIT := 1 47 | else ifeq ($(USE),10.11-xcode7.3) 48 | USE_DIRS := i386 x86_64 fat-i386-x86_64 49 | NO_DELAY_INIT := 1 50 | else ifeq ($(USE),15-xcode16.0) 51 | USE_DIRS := x86_64 52 | NO_LAZY := 1 53 | else 54 | # Warn about unspecified subset, but effectively fall back to 10.11-xcode7.3. 55 | $(warning USE - Option either unset or invalid. Using a safe fallback.) 56 | $(warning USE - Valid choices: all, 10.6-xcode3.2.6, 10.11-xcode7.3, 15-xcode16.0.) 57 | USE_DIRS := i386 x86_64 fat-i386-x86_64 58 | NO_DELAY_INIT := 1 59 | NO_LAZY := 1 60 | endif 61 | 62 | ifeq ($(NO_DELAY_INIT),) 63 | TARGET_FILES += dylib_use_command-weak-delay.bin 64 | endif 65 | 66 | # Setup target names from all/used architecture directories. 67 | ALL_TARGETS := $(addprefix all-,$(ALL_DIRS)) 68 | USE_TARGETS := $(addprefix all-,$(USE_DIRS)) 69 | 70 | # Tweak flags according to toolchain support. 71 | LIBEXTRA_LDADD = -L$(@D) 72 | ifeq ($(NO_UPWARD),1) 73 | # Xcode 3.2.6: `ld` doesn't support `-upward_library`. 74 | LIBEXTRA_LDADD += -Wl,-weak_library,/usr/lib/libz.dylib 75 | else 76 | LIBEXTRA_LDADD += -Wl,-upward_library,/usr/lib/libz.dylib 77 | endif 78 | ifeq ($(NO_LAZY), 1) 79 | # Xcode 3.2.6: `ld` theoretically supports `-lazy-l`, but gets confused. 80 | LIBEXTRA_LDADD += -Wl,-reexport-lhello 81 | else 82 | LIBEXTRA_LDADD += -Wl,-lazy-lhello 83 | endif 84 | 85 | # Setup default target (used subset). 86 | .PHONY: all 87 | all: $(USE_TARGETS) inconsistent 88 | 89 | # Setup targets that build all files for a given architecture (`all-`). 90 | .PHONY: $(ALL_TARGETS) 91 | $(ALL_TARGETS): all-%: $(addprefix %/,$(TARGET_FILES)) 92 | 93 | # Setup targets for creating architecture-specific output directories. 94 | $(ALL_DIRS): 95 | mkdir -p $@ 96 | 97 | # Setup architecture-specific per-file targets (`/`). 98 | %/hello.o: $(HELLO_SRC) % 99 | $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -c $< 100 | 101 | %/hello.bin: $(HELLO_SRC) % 102 | $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ $(RPATH_FLAGS) $< 103 | 104 | %/hello_expected.bin: %/hello.bin 105 | cp $< $@ 106 | install_name_tool -change /usr/lib/libSystem.B.dylib test $@ 107 | 108 | %/hello_rpath_expected.bin: %/hello.bin 109 | cp $< $@ 110 | install_name_tool -rpath made_up_path /usr/lib $@ 111 | 112 | %/dylib_use_command-weak-delay.bin: $(HELLO_SRC) % 113 | $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -Wl,-weak-l,z -Wl,-delay-l,z $< 114 | 115 | %/libhello.dylib: $(LIBHELLO_SRC) % 116 | $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -dynamiclib $< 117 | 118 | %/libhello_expected.dylib: %/libhello.dylib 119 | cp $< $@ 120 | install_name_tool -id test $@ 121 | 122 | %/libextrahello.dylib: $(LIBHELLO_SRC) % %/libhello.dylib 123 | $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -dynamiclib $< $(LIBEXTRA_LDADD) 124 | 125 | %/hellobundle.so: $(LIBHELLO_SRC) % 126 | $(CC) $(CFLAGS) $(ARCH_FLAGS) -bundle $< -o $@ 127 | 128 | # build inconsistent binaries 129 | .PHONY: inconsistent 130 | inconsistent: $(USE_TARGETS) 131 | ./make-inconsistent.sh $(USE_DIRS) 132 | 133 | # Remove all build products, even those not selected in the current run. 134 | .PHONY: clean 135 | clean: 136 | rm -rf $(ALL_DIRS) 137 | 138 | # Copy all selected build products to their final destination. 139 | .PHONY: install 140 | install: all 141 | cp -rf $(USE_DIRS) ../bin/ 142 | -------------------------------------------------------------------------------- /lib/macho/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # A collection of utility functions used throughout ruby-macho. 5 | module Utils 6 | # Rounds a value to the next multiple of the given round. 7 | # @param value [Integer] the number being rounded 8 | # @param round [Integer] the number being rounded with 9 | # @return [Integer] the rounded value 10 | # @see http://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c 11 | def self.round(value, round) 12 | round -= 1 13 | value += round 14 | value &= ~round 15 | value 16 | end 17 | 18 | # Returns the number of bytes needed to pad the given size to the given 19 | # alignment. 20 | # @param size [Integer] the unpadded size 21 | # @param alignment [Integer] the number to alignment the size with 22 | # @return [Integer] the number of pad bytes required 23 | def self.padding_for(size, alignment) 24 | round(size, alignment) - size 25 | end 26 | 27 | # Returns a string of null bytes of the requested (non-negative) size 28 | # @param size [Integer] the size of the nullpad 29 | # @return [String] the null string (or empty string, for `size = 0`) 30 | # @raise [ArgumentError] if a non-positive nullpad is requested 31 | def self.nullpad(size) 32 | raise ArgumentError, "size < 0: #{size}" if size.negative? 33 | 34 | "\x00" * size 35 | end 36 | 37 | # Converts an abstract (native-endian) String#unpack format to big or 38 | # little. 39 | # @param format [String] the format string being converted 40 | # @param endianness [Symbol] either `:big` or `:little` 41 | # @return [String] the converted string 42 | def self.specialize_format(format, endianness) 43 | modifier = endianness == :big ? ">" : "<" 44 | format.tr("=", modifier) 45 | end 46 | 47 | # Packs tagged strings into an aligned payload. 48 | # @param fixed_offset [Integer] the baseline offset for the first packed 49 | # string 50 | # @param alignment [Integer] the alignment value to use for packing 51 | # @param strings [Hash] the labeled strings to pack 52 | # @return [Array] the packed string and labeled offsets 53 | def self.pack_strings(fixed_offset, alignment, strings = {}) 54 | offsets = {} 55 | next_offset = fixed_offset 56 | payload = +"" 57 | 58 | strings.each do |key, string| 59 | offsets[key] = next_offset 60 | payload << string 61 | payload << Utils.nullpad(1) 62 | next_offset += string.bytesize + 1 63 | end 64 | 65 | payload << Utils.nullpad(padding_for(fixed_offset + payload.bytesize, alignment)) 66 | [payload.freeze, offsets] 67 | end 68 | 69 | # Compares the given number to valid Mach-O magic numbers. 70 | # @param num [Integer] the number being checked 71 | # @return [Boolean] whether `num` is a valid Mach-O magic number 72 | def self.magic?(num) 73 | Headers::MH_MAGICS.key?(num) 74 | end 75 | 76 | # Compares the given number to valid Fat magic numbers. 77 | # @param num [Integer] the number being checked 78 | # @return [Boolean] whether `num` is a valid Fat magic number 79 | def self.fat_magic?(num) 80 | [Headers::FAT_MAGIC, Headers::FAT_MAGIC_64].include? num 81 | end 82 | 83 | # Compares the given number to valid 32-bit Fat magic numbers. 84 | # @param num [Integer] the number being checked 85 | # @return [Boolean] whether `num` is a valid 32-bit fat magic number 86 | def self.fat_magic32?(num) 87 | num == Headers::FAT_MAGIC 88 | end 89 | 90 | # Compares the given number to valid 64-bit Fat magic numbers. 91 | # @param num [Integer] the number being checked 92 | # @return [Boolean] whether `num` is a valid 64-bit fat magic number 93 | def self.fat_magic64?(num) 94 | num == Headers::FAT_MAGIC_64 95 | end 96 | 97 | # Compares the given number to valid 32-bit Mach-O magic numbers. 98 | # @param num [Integer] the number being checked 99 | # @return [Boolean] whether `num` is a valid 32-bit magic number 100 | def self.magic32?(num) 101 | [Headers::MH_MAGIC, Headers::MH_CIGAM].include? num 102 | end 103 | 104 | # Compares the given number to valid 64-bit Mach-O magic numbers. 105 | # @param num [Integer] the number being checked 106 | # @return [Boolean] whether `num` is a valid 64-bit magic number 107 | def self.magic64?(num) 108 | [Headers::MH_MAGIC_64, Headers::MH_CIGAM_64].include? num 109 | end 110 | 111 | # Compares the given number to valid little-endian magic numbers. 112 | # @param num [Integer] the number being checked 113 | # @return [Boolean] whether `num` is a valid little-endian magic number 114 | def self.little_magic?(num) 115 | [Headers::MH_CIGAM, Headers::MH_CIGAM_64].include? num 116 | end 117 | 118 | # Compares the given number to valid big-endian magic numbers. 119 | # @param num [Integer] the number being checked 120 | # @return [Boolean] whether `num` is a valid big-endian magic number 121 | def self.big_magic?(num) 122 | [Headers::MH_MAGIC, Headers::MH_MAGIC_64].include? num 123 | end 124 | 125 | # Compares the given number to the known magic number for a compressed Mach-O slice. 126 | # @param num [Integer] the number being checked 127 | # @return [Boolean] whether `num` is a valid compressed header magic number 128 | def self.compressed_magic?(num) 129 | num == Headers::COMPRESSED_MAGIC 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/test_serialize_load_commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class MachOLoadCommandSerializationTest < Minitest::Test 6 | include Helpers 7 | 8 | def test_can_serialize 9 | filename = fixture(:i386, "hello.bin") 10 | file = MachO::MachOFile.new(filename) 11 | lc = file[:LC_SEGMENT].first 12 | 13 | refute lc.serializable? 14 | 15 | assert_raises MachO::LoadCommandNotSerializableError do 16 | lc.serialize(MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file)) 17 | end 18 | end 19 | 20 | def test_serialize_segment 21 | skip 22 | end 23 | 24 | def test_serialize_symtab 25 | skip 26 | end 27 | 28 | def test_serialize_symseg 29 | skip 30 | end 31 | 32 | def test_serialize_thread 33 | skip 34 | end 35 | 36 | def test_serialize_unixthread 37 | skip 38 | end 39 | 40 | def test_serialize_loadfvmlib 41 | skip 42 | end 43 | 44 | def test_serialize_ident 45 | skip 46 | end 47 | 48 | def test_serialize_fvmfile 49 | skip 50 | end 51 | 52 | def test_serialize_prepage 53 | skip 54 | end 55 | 56 | def test_serialize_dysymtab 57 | skip 58 | end 59 | 60 | def test_serialize_load_dylib 61 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 62 | 63 | filenames.each do |filename| 64 | file = MachO::MachOFile.new(filename) 65 | ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file) 66 | lc = file[:LC_LOAD_DYLIB].first 67 | lc2 = MachO::LoadCommands::LoadCommand.create(:LC_LOAD_DYLIB, lc.name.to_s, 68 | lc.timestamp, lc.current_version, 69 | lc.compatibility_version) 70 | blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] 71 | 72 | assert_instance_of lc.class, lc2 73 | assert_equal blob, lc.serialize(ctx) 74 | assert_equal blob, lc2.serialize(ctx) 75 | end 76 | end 77 | 78 | def test_serialize_load_dylib_new 79 | filenames = SINGLE_64_ARCHES.map { |a| fixture(a, "dylib_use_command-weak-delay.bin") } 80 | 81 | filenames.each do |filename| 82 | file = MachO::MachOFile.new(filename) 83 | ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file) 84 | lc = file[:LC_LOAD_WEAK_DYLIB].first 85 | lc2 = MachO::LoadCommands::LoadCommand.create(:LC_LOAD_WEAK_DYLIB, lc.name.to_s, 86 | lc.marker, lc.current_version, 87 | lc.compatibility_version, lc.flags) 88 | blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] 89 | 90 | assert_instance_of lc.class, lc2 91 | assert_equal blob, lc.serialize(ctx) 92 | assert_equal blob, lc2.serialize(ctx) 93 | end 94 | end 95 | 96 | def test_serialize_id_dylib 97 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "libhello.dylib") } 98 | 99 | filenames.each do |filename| 100 | file = MachO::MachOFile.new(filename) 101 | ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file) 102 | lc = file[:LC_ID_DYLIB].first 103 | lc2 = MachO::LoadCommands::LoadCommand.create(:LC_ID_DYLIB, lc.name.to_s, 104 | lc.timestamp, lc.current_version, 105 | lc.compatibility_version) 106 | blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] 107 | 108 | assert_equal blob, lc.serialize(ctx) 109 | assert_equal blob, lc2.serialize(ctx) 110 | end 111 | end 112 | 113 | def test_serialize_load_dylinker 114 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 115 | 116 | filenames.each do |filename| 117 | file = MachO::MachOFile.new(filename) 118 | ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file) 119 | lc = file[:LC_LOAD_DYLINKER].first 120 | lc2 = MachO::LoadCommands::LoadCommand.create(:LC_LOAD_DYLINKER, lc.name.to_s) 121 | blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] 122 | 123 | assert_equal blob, lc.serialize(ctx) 124 | assert_equal blob, lc2.serialize(ctx) 125 | end 126 | end 127 | 128 | def test_serialize_id_dylinker 129 | skip 130 | end 131 | 132 | def test_serialize_prebound_dylib 133 | skip 134 | end 135 | 136 | def test_serialize_routines 137 | skip 138 | end 139 | 140 | def test_serialize_sub_framework 141 | skip 142 | end 143 | 144 | def test_serialize_sub_umbrella 145 | skip 146 | end 147 | 148 | def test_serialize_sub_client 149 | skip 150 | end 151 | 152 | def test_serialize_sub_library 153 | skip 154 | end 155 | 156 | def test_serialize_twolevel_hints 157 | skip 158 | end 159 | 160 | def test_serialize_prebind_cksum 161 | skip 162 | end 163 | 164 | def test_serialize_load_weak_dylib 165 | skip 166 | end 167 | 168 | def test_serialize_segment_64 169 | skip 170 | end 171 | 172 | def test_serialize_routines_64 173 | skip 174 | end 175 | 176 | def test_serialize_uuid 177 | skip 178 | end 179 | 180 | def test_serialize_rpath 181 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 182 | 183 | filenames.each do |filename| 184 | file = MachO::MachOFile.new(filename) 185 | ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file) 186 | lc = file[:LC_RPATH].first 187 | lc2 = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, lc.path.to_s) 188 | blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] 189 | 190 | assert_equal blob, lc.serialize(ctx) 191 | assert_equal blob, lc2.serialize(ctx) 192 | end 193 | end 194 | 195 | def test_serialize_code_signature 196 | skip 197 | end 198 | 199 | def test_serialize_segment_split_info 200 | skip 201 | end 202 | 203 | def test_serialize_reexport_dylib 204 | skip 205 | end 206 | 207 | def test_serialize_lazy_load_dylib 208 | skip 209 | end 210 | 211 | def test_serialize_encryption_info 212 | skip 213 | end 214 | 215 | def test_serialize_dyld_info 216 | skip 217 | end 218 | 219 | def test_serialize_dyld_info_only 220 | skip 221 | end 222 | 223 | def test_serialize_load_upward_dylib 224 | skip 225 | end 226 | 227 | def test_serialize_version_min_macosx 228 | skip 229 | end 230 | 231 | def test_serialize_version_min_iphoneos 232 | skip 233 | end 234 | 235 | def test_serialize_function_starts 236 | skip 237 | end 238 | 239 | def test_serialize_dyld_environment 240 | skip 241 | end 242 | 243 | def test_serialize_main 244 | skip 245 | end 246 | 247 | def test_serialize_data_in_code 248 | skip 249 | end 250 | 251 | def test_serialize_source_version 252 | skip 253 | end 254 | 255 | def test_serialize_dylib_code_sign_drs 256 | skip 257 | end 258 | 259 | def test_serialize_encryption_info_64 260 | skip 261 | end 262 | 263 | def test_serialize_linker_option 264 | skip 265 | end 266 | 267 | def test_serialize_linker_optimization_hint 268 | skip 269 | end 270 | 271 | def test_serialize_version_min_tvos 272 | skip 273 | end 274 | 275 | def test_serialize_version_min_watchos 276 | skip 277 | end 278 | end 279 | -------------------------------------------------------------------------------- /lib/macho/sections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # Classes and constants for parsing sections in Mach-O binaries. 5 | module Sections 6 | # type mask 7 | SECTION_TYPE_MASK = 0x000000ff 8 | 9 | # attributes mask 10 | SECTION_ATTRIBUTES_MASK = 0xffffff00 11 | 12 | # user settable attributes mask 13 | SECTION_ATTRIBUTES_USR_MASK = 0xff000000 14 | 15 | # system settable attributes mask 16 | SECTION_ATTRIBUTES_SYS_MASK = 0x00ffff00 17 | 18 | # maximum specifiable section alignment, as a power of 2 19 | # @note see `MAXSECTALIGN` macro in `cctools/misc/lipo.c` 20 | MAX_SECT_ALIGN = 15 21 | 22 | # association of section type symbols to values 23 | # @api private 24 | SECTION_TYPES = { 25 | :S_REGULAR => 0x0, 26 | :S_ZEROFILL => 0x1, 27 | :S_CSTRING_LITERALS => 0x2, 28 | :S_4BYTE_LITERALS => 0x3, 29 | :S_8BYTE_LITERALS => 0x4, 30 | :S_LITERAL_POINTERS => 0x5, 31 | :S_NON_LAZY_SYMBOL_POINTERS => 0x6, 32 | :S_LAZY_SYMBOL_POINTERS => 0x7, 33 | :S_SYMBOL_STUBS => 0x8, 34 | :S_MOD_INIT_FUNC_POINTERS => 0x9, 35 | :S_MOD_TERM_FUNC_POINTERS => 0xa, 36 | :S_COALESCED => 0xb, 37 | :S_GB_ZEROFILE => 0xc, 38 | :S_INTERPOSING => 0xd, 39 | :S_16BYTE_LITERALS => 0xe, 40 | :S_DTRACE_DOF => 0xf, 41 | :S_LAZY_DYLIB_SYMBOL_POINTERS => 0x10, 42 | :S_THREAD_LOCAL_REGULAR => 0x11, 43 | :S_THREAD_LOCAL_ZEROFILL => 0x12, 44 | :S_THREAD_LOCAL_VARIABLES => 0x13, 45 | :S_THREAD_LOCAL_VARIABLE_POINTERS => 0x14, 46 | :S_THREAD_LOCAL_INIT_FUNCTION_POINTERS => 0x15, 47 | :S_INIT_FUNC_OFFSETS => 0x16, 48 | }.freeze 49 | 50 | # association of section attribute symbols to values 51 | # @api private 52 | SECTION_ATTRIBUTES = { 53 | :S_ATTR_PURE_INSTRUCTIONS => 0x80000000, 54 | :S_ATTR_NO_TOC => 0x40000000, 55 | :S_ATTR_STRIP_STATIC_SYMS => 0x20000000, 56 | :S_ATTR_NO_DEAD_STRIP => 0x10000000, 57 | :S_ATTR_LIVE_SUPPORT => 0x08000000, 58 | :S_ATTR_SELF_MODIFYING_CODE => 0x04000000, 59 | :S_ATTR_DEBUG => 0x02000000, 60 | :S_ATTR_SOME_INSTRUCTIONS => 0x00000400, 61 | :S_ATTR_EXT_RELOC => 0x00000200, 62 | :S_ATTR_LOC_RELOC => 0x00000100, 63 | }.freeze 64 | 65 | # association of section flag symbols to values 66 | # @api private 67 | SECTION_FLAGS = { 68 | **SECTION_TYPES, 69 | **SECTION_ATTRIBUTES, 70 | }.freeze 71 | 72 | # association of section name symbols to names 73 | # @api private 74 | SECTION_NAMES = { 75 | :SECT_TEXT => "__text", 76 | :SECT_FVMLIB_INIT0 => "__fvmlib_init0", 77 | :SECT_FVMLIB_INIT1 => "__fvmlib_init1", 78 | :SECT_DATA => "__data", 79 | :SECT_BSS => "__bss", 80 | :SECT_COMMON => "__common", 81 | :SECT_OBJC_SYMBOLS => "__symbol_table", 82 | :SECT_OBJC_MODULES => "__module_info", 83 | :SECT_OBJC_STRINGS => "__selector_strs", 84 | :SECT_OBJC_REFS => "__selector_refs", 85 | :SECT_ICON_HEADER => "__header", 86 | :SECT_ICON_TIFF => "__tiff", 87 | }.freeze 88 | 89 | # Represents a section of a segment for 32-bit architectures. 90 | class Section < MachOStructure 91 | # @return [String] the name of the section, including null pad bytes 92 | field :sectname, :string, :padding => :null, :size => 16 93 | 94 | # @return [String] the name of the segment's section, including null 95 | # pad bytes 96 | field :segname, :string, :padding => :null, :size => 16 97 | 98 | # @return [Integer] the memory address of the section 99 | field :addr, :uint32 100 | 101 | # @return [Integer] the size, in bytes, of the section 102 | field :size, :uint32 103 | 104 | # @return [Integer] the file offset of the section 105 | field :offset, :uint32 106 | 107 | # @return [Integer] the section alignment (power of 2) of the section 108 | field :align, :uint32 109 | 110 | # @return [Integer] the file offset of the section's relocation entries 111 | field :reloff, :uint32 112 | 113 | # @return [Integer] the number of relocation entries 114 | field :nreloc, :uint32 115 | 116 | # @return [Integer] flags for type and attributes of the section 117 | field :flags, :uint32 118 | 119 | # @return [void] reserved (for offset or index) 120 | field :reserved1, :uint32 121 | 122 | # @return [void] reserved (for count or sizeof) 123 | field :reserved2, :uint32 124 | 125 | # @return [String] the section's name 126 | def section_name 127 | sectname 128 | end 129 | 130 | # @return [String] the parent segment's name 131 | def segment_name 132 | segname 133 | end 134 | 135 | # @return [Boolean] whether the section is empty (i.e, {size} is 0) 136 | def empty? 137 | size.zero? 138 | end 139 | 140 | # @return [Integer] the raw numeric type of this section 141 | def type 142 | flags & SECTION_TYPE_MASK 143 | end 144 | 145 | # @example 146 | # puts "this section is regular" if sect.type?(:S_REGULAR) 147 | # @param type_sym [Symbol] a section type symbol 148 | # @return [Boolean] whether this section is of the given type 149 | def type?(type_sym) 150 | type == SECTION_TYPES[type_sym] 151 | end 152 | 153 | # @return [Integer] the raw numeric attributes of this section 154 | def attributes 155 | flags & SECTION_ATTRIBUTES_MASK 156 | end 157 | 158 | # @example 159 | # puts "pure instructions" if sect.attribute?(:S_ATTR_PURE_INSTRUCTIONS) 160 | # @param attr_sym [Symbol] a section attribute symbol 161 | # @return [Boolean] whether this section is of the given type 162 | def attribute?(attr_sym) 163 | !!(attributes & SECTION_ATTRIBUTES[attr_sym]) 164 | end 165 | 166 | # @deprecated Use {#type?} or {#attribute?} instead. 167 | # @example 168 | # puts "this section is regular" if sect.flag?(:S_REGULAR) 169 | # @param flag [Symbol] a section flag symbol 170 | # @return [Boolean] whether the flag is present in the section's {flags} 171 | def flag?(flag) 172 | flag = SECTION_FLAGS[flag] 173 | 174 | return false if flag.nil? 175 | 176 | flags & flag == flag 177 | end 178 | 179 | # @return [Hash] a hash representation of this {Section} 180 | def to_h 181 | { 182 | "sectname" => sectname, 183 | "segname" => segname, 184 | "addr" => addr, 185 | "size" => size, 186 | "offset" => offset, 187 | "align" => align, 188 | "reloff" => reloff, 189 | "nreloc" => nreloc, 190 | "flags" => flags, 191 | "reserved1" => reserved1, 192 | "reserved2" => reserved2, 193 | }.merge super 194 | end 195 | end 196 | 197 | # Represents a section of a segment for 64-bit architectures. 198 | class Section64 < Section 199 | # @return [Integer] the memory address of the section 200 | field :addr, :uint64 201 | 202 | # @return [Integer] the size, in bytes, of the section 203 | field :size, :uint64 204 | 205 | # @return [void] reserved 206 | field :reserved3, :uint32 207 | 208 | # @return [Hash] a hash representation of this {Section64} 209 | def to_h 210 | { 211 | "reserved3" => reserved3, 212 | }.merge super 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/macho/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # A generic Mach-O error in execution. 5 | class MachOError < RuntimeError 6 | end 7 | 8 | # Raised when a Mach-O file modification fails. 9 | class ModificationError < MachOError 10 | end 11 | 12 | # Raised when codesigning fails. Certain environments 13 | # may want to rescue this to treat it as non-fatal. 14 | class CodeSigningError < MachOError 15 | end 16 | 17 | # Raised when a Mach-O file modification fails but can be recovered when 18 | # operating on multiple Mach-O slices of a fat binary in non-strict mode. 19 | class RecoverableModificationError < ModificationError 20 | # @return [Integer, nil] The index of the Mach-O slice of a fat binary for 21 | # which modification failed or `nil` if not a fat binary. This is used to 22 | # make the error message more useful. 23 | attr_accessor :macho_slice 24 | 25 | # @return [String] The exception message. 26 | def to_s 27 | s = super.to_s 28 | s = "While modifying Mach-O slice #{@macho_slice}: #{s}" if @macho_slice 29 | s 30 | end 31 | end 32 | 33 | # Raised when a file is not a Mach-O. 34 | class NotAMachOError < MachOError 35 | end 36 | 37 | # Raised when a file is too short to be a valid Mach-O file. 38 | class TruncatedFileError < NotAMachOError 39 | def initialize 40 | super("File is too short to be a valid Mach-O") 41 | end 42 | end 43 | 44 | # Raised when a file's magic bytes are not valid Mach-O magic. 45 | class MagicError < NotAMachOError 46 | # @param num [Integer] the unknown number 47 | def initialize(magic) 48 | super("Unrecognized Mach-O magic: 0x%02x" % { :magic => magic }) 49 | end 50 | end 51 | 52 | # Raised when a file is a Java classfile instead of a fat Mach-O. 53 | class JavaClassFileError < NotAMachOError 54 | def initialize 55 | super("File is a Java class file") 56 | end 57 | end 58 | 59 | # Raised when a a fat Mach-O file has zero architectures 60 | class ZeroArchitectureError < NotAMachOError 61 | def initialize 62 | super("Fat file has zero internal architectures") 63 | end 64 | end 65 | 66 | # Raised when there is a mismatch between the fat arch 67 | # and internal slice cputype or cpusubtype. 68 | class CPUTypeMismatchError < NotAMachOError 69 | def initialize(fat_cputype, fat_cpusubtype, macho_cputype, macho_cpusubtype) 70 | # @param cputype_fat [Integer] the CPU type in the fat header 71 | # @param cpusubtype_fat [Integer] the CPU subtype in the fat header 72 | # @param cputype_macho [Integer] the CPU type in the macho header 73 | # @param cpusubtype_macho [Integer] the CPU subtype in the macho header 74 | super("Mismatch between cputypes >> 0x%08x and 0x%08x\n" \ 75 | "and/or cpusubtypes >> 0x%08x and 0x%08x" % 76 | { :fat_cputype => fat_cputype, :macho_cputype => macho_cputype, 77 | :fat_cpusubtype => fat_cpusubtype, :macho_cpusubtype => macho_cpusubtype }) 78 | end 79 | end 80 | 81 | # Raised when a fat binary is loaded with MachOFile. 82 | class FatBinaryError < MachOError 83 | def initialize 84 | super("Fat binaries must be loaded with MachO::FatFile") 85 | end 86 | end 87 | 88 | # Raised when a Mach-O is loaded with FatFile. 89 | class MachOBinaryError < MachOError 90 | def initialize 91 | super("Normal binaries must be loaded with MachO::MachOFile") 92 | end 93 | end 94 | 95 | # Raised when the CPU type is unknown. 96 | class CPUTypeError < MachOError 97 | # @param cputype [Integer] the unknown CPU type 98 | def initialize(cputype) 99 | super("Unrecognized CPU type: 0x%08x" % { :cputype => cputype }) 100 | end 101 | end 102 | 103 | # Raised when the CPU type/sub-type pair is unknown. 104 | class CPUSubtypeError < MachOError 105 | # @param cputype [Integer] the CPU type of the unknown pair 106 | # @param cpusubtype [Integer] the CPU sub-type of the unknown pair 107 | def initialize(cputype, cpusubtype) 108 | super("Unrecognized CPU sub-type: 0x%08x " \ 109 | "(for CPU type: 0x%08x" % { :cputype => cputype, :cpusubtype => cpusubtype }) 110 | end 111 | end 112 | 113 | # Raised when a mach-o file's filetype field is unknown. 114 | class FiletypeError < MachOError 115 | # @param num [Integer] the unknown number 116 | def initialize(num) 117 | super("Unrecognized Mach-O filetype code: 0x%02x" % { :num => num }) 118 | end 119 | end 120 | 121 | # Raised when an unknown load command is encountered. 122 | class LoadCommandError < MachOError 123 | # @param num [Integer] the unknown number 124 | def initialize(num) 125 | super("Unrecognized Mach-O load command: 0x%02x" % { :num => num }) 126 | end 127 | end 128 | 129 | # Raised when a load command can't be created manually. 130 | class LoadCommandNotCreatableError < MachOError 131 | # @param cmd_sym [Symbol] the uncreatable load command's symbol 132 | def initialize(cmd_sym) 133 | super("Load commands of type #{cmd_sym} cannot be created manually") 134 | end 135 | end 136 | 137 | # Raised when the number of arguments used to create a load command manually 138 | # is wrong. 139 | class LoadCommandCreationArityError < MachOError 140 | # @param cmd_sym [Symbol] the load command's symbol 141 | # @param expected_arity [Integer] the number of arguments expected 142 | # @param actual_arity [Integer] the number of arguments received 143 | def initialize(cmd_sym, expected_arity, actual_arity) 144 | super("Expected #{expected_arity} arguments for #{cmd_sym} creation, " \ 145 | "got #{actual_arity}") 146 | end 147 | end 148 | 149 | # Raised when a load command can't be serialized. 150 | class LoadCommandNotSerializableError < MachOError 151 | # @param cmd_sym [Symbol] the load command's symbol 152 | def initialize(cmd_sym) 153 | super("Load commands of type #{cmd_sym} cannot be serialized") 154 | end 155 | end 156 | 157 | # Raised when a load command string is malformed in some way. 158 | class LCStrMalformedError < MachOError 159 | # @param lc [MachO::LoadCommand] the load command containing the string 160 | def initialize(lc) 161 | super("Load command #{lc.type} at offset #{lc.view.offset} contains a " \ 162 | "malformed string") 163 | end 164 | end 165 | 166 | # Raised when a change at an offset is not valid. 167 | class OffsetInsertionError < ModificationError 168 | # @param offset [Integer] the invalid offset 169 | def initialize(offset) 170 | super("Insertion at offset #{offset} is not valid") 171 | end 172 | end 173 | 174 | # Raised when load commands are too large to fit in the current file. 175 | class HeaderPadError < ModificationError 176 | # @param filename [String] the filename 177 | def initialize(filename) 178 | super("Updated load commands do not fit in the header of " \ 179 | "#{filename}. #{filename} needs to be relinked, possibly with " \ 180 | "-headerpad or -headerpad_max_install_names") 181 | end 182 | end 183 | 184 | # Raised when attempting to change a dylib name that doesn't exist. 185 | class DylibUnknownError < RecoverableModificationError 186 | # @param dylib [String] the unknown shared library name 187 | def initialize(dylib) 188 | super("No such dylib name: #{dylib}") 189 | end 190 | end 191 | 192 | # Raised when a dylib is missing an ID 193 | class DylibIdMissingError < RecoverableModificationError 194 | def initialize 195 | super("Dylib is missing a dylib ID") 196 | end 197 | end 198 | 199 | # Raised when attempting to change an rpath that doesn't exist. 200 | class RpathUnknownError < RecoverableModificationError 201 | # @param path [String] the unknown runtime path 202 | def initialize(path) 203 | super("No such runtime path: #{path}") 204 | end 205 | end 206 | 207 | # Raised when attempting to add an rpath that already exists. 208 | class RpathExistsError < RecoverableModificationError 209 | # @param path [String] the extant path 210 | def initialize(path) 211 | super("#{path} already exists") 212 | end 213 | end 214 | 215 | # Raised whenever unfinished code is called. 216 | class UnimplementedError < MachOError 217 | # @param thing [String] the thing that is unimplemented 218 | def initialize(thing) 219 | super("Unimplemented: #{thing}") 220 | end 221 | end 222 | 223 | # Raised when attempting to create a {FatFile} from one or more {MachOFile}s 224 | # whose offsets will not fit within the resulting 32-bit {Headers::FatArch#offset} fields. 225 | class FatArchOffsetOverflowError < MachOError 226 | # @param offset [Integer] the offending offset 227 | def initialize(offset) 228 | super("Offset #{offset} exceeds the 32-bit width of a fat_arch offset. " \ 229 | "Consider merging with `fat64: true`") 230 | end 231 | end 232 | 233 | # Raised when attempting to parse a compressed Mach-O without explicitly 234 | # requesting decompression. 235 | class CompressedMachOError < MachOError 236 | end 237 | 238 | # Raised when attempting to decompress a compressed Mach-O without adequate 239 | # dependencies, or on other decompression errors. 240 | class DecompressionError < MachOError 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /test/bench.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | require "benchmark/ips" 5 | 6 | class RubyMachOBenchmark 7 | include Helpers 8 | 9 | def run 10 | unless installed?("otool") && installed?("install_name_tool") 11 | puts "otool and install_name_tool are required to run benchmarks." 12 | return 13 | end 14 | 15 | bench_get_id 16 | bench_get_dylib 17 | bench_set_id 18 | bench_set_dylib 19 | bench_add_rpath 20 | bench_delete_rpath 21 | bench_change_rpath 22 | 23 | bench_fat_get_id 24 | bench_fat_get_dylib 25 | bench_fat_set_id 26 | bench_fat_set_dylib 27 | bench_fat_add_rpath 28 | bench_fat_delete_rpath 29 | bench_fat_change_rpath 30 | end 31 | 32 | def bench_get_id 33 | filename = fixture(:x86_64, "libhello.dylib") 34 | 35 | Benchmark.ips do |bm| 36 | bm.report("otool_get_id") do 37 | libs = `otool -L #{filename}`.split("\n") 38 | libs.shift 39 | libs.shift[OTOOL_RX, 1] 40 | end 41 | 42 | bm.report("ruby_get_id") do 43 | MachO.open(filename).dylib_id 44 | end 45 | 46 | bm.compare! 47 | end 48 | end 49 | 50 | def bench_get_dylib 51 | filename = fixture(:x86_64, "libhello.dylib") 52 | 53 | Benchmark.ips do |bm| 54 | bm.report("otool_get_dylib") do 55 | libs = `otool -L #{filename}`.split("\n") 56 | libs.shift(2) 57 | libs.map! { |lib| lib[OTOOL_RX, 1] }.compact! 58 | end 59 | 60 | bm.report("ruby_get_dylib") do 61 | MachO::Tools.dylibs(filename) 62 | end 63 | 64 | bm.compare! 65 | end 66 | end 67 | 68 | def bench_set_id 69 | filename = fixture(:x86_64, "libhello.dylib") 70 | benchfile = "#{filename}.bench" 71 | FileUtils.cp(filename, benchfile) 72 | i = 0 73 | 74 | Benchmark.ips do |bm| 75 | bm.report("int_set_id") do 76 | `install_name_tool -id #{i += 1} #{benchfile}` 77 | end 78 | 79 | i = 0 80 | 81 | bm.report("ruby_set_id") do 82 | MachO::Tools.change_dylib_id(benchfile, (i += 1).to_s) 83 | end 84 | 85 | bm.compare! 86 | end 87 | ensure 88 | delete_if_exists(benchfile) 89 | end 90 | 91 | def bench_set_dylib 92 | filename = fixture(:x86_64, "libhello.dylib") 93 | benchfile = "#{filename}.bench" 94 | FileUtils.cp(filename, benchfile) 95 | i = 0 96 | 97 | Benchmark.ips do |bm| 98 | MachO::Tools.change_install_name(benchfile, "/usr/lib/libSystem.B.dylib", "0") 99 | 100 | bm.report("int_set_dylib") do 101 | `install_name_tool -change #{i} #{i += 1} #{benchfile}` 102 | end 103 | 104 | MachO::Tools.change_install_name(benchfile, i.to_s, "0") 105 | i = 0 106 | 107 | bm.report("ruby_set_dylib") do 108 | MachO::Tools.change_install_name(benchfile, i.to_s, (i += 1).to_s) 109 | end 110 | 111 | bm.compare! 112 | end 113 | ensure 114 | delete_if_exists(benchfile) 115 | end 116 | 117 | def bench_add_rpath 118 | filename = fixture(:x86_64, "libhello.dylib") 119 | benchfile = "#{filename}.bench" 120 | i = 0 121 | 122 | Benchmark.ips do |bm| 123 | bm.report("int_add_rpath") do 124 | FileUtils.cp(filename, benchfile) 125 | `install_name_tool -add_rpath #{i += 1} #{benchfile}` 126 | FileUtils.rm(benchfile) 127 | end 128 | 129 | bm.report("ruby_add_rpath") do 130 | FileUtils.cp(filename, benchfile) 131 | MachO::Tools.add_rpath(benchfile, (i += 1).to_s) 132 | FileUtils.rm(benchfile) 133 | end 134 | 135 | bm.compare! 136 | end 137 | ensure 138 | delete_if_exists(benchfile) 139 | end 140 | 141 | def bench_delete_rpath 142 | filename = fixture(:x86_64, "hello.bin") 143 | benchfile = "#{filename}.bench" 144 | rpath = MachO.open(filename).rpaths.first 145 | 146 | Benchmark.ips do |bm| 147 | bm.report("int_del_rpath") do 148 | FileUtils.cp(filename, benchfile) 149 | `install_name_tool -delete_rpath #{rpath} #{benchfile}` 150 | FileUtils.rm(benchfile) 151 | end 152 | 153 | bm.report("ruby_del_rpath") do 154 | FileUtils.cp(filename, benchfile) 155 | MachO::Tools.delete_rpath(benchfile, rpath) 156 | FileUtils.rm(benchfile) 157 | end 158 | 159 | bm.compare! 160 | end 161 | ensure 162 | delete_if_exists(benchfile) 163 | end 164 | 165 | def bench_change_rpath 166 | filename = fixture(:x86_64, "hello.bin") 167 | benchfile = "#{filename}.bench" 168 | FileUtils.cp(filename, benchfile) 169 | rpath = MachO.open(filename).rpaths.first 170 | i = 0 171 | 172 | MachO::Tools.change_rpath(benchfile, rpath, i.to_s) 173 | 174 | Benchmark.ips do |bm| 175 | bm.report("int_change_rpath") do 176 | `install_name_tool -rpath #{i} #{i += 1} #{benchfile}` 177 | end 178 | 179 | bm.report("ruby_change_rpath") do 180 | MachO::Tools.change_rpath(benchfile, i.to_s, (i += 1).to_s) 181 | end 182 | 183 | bm.compare! 184 | end 185 | ensure 186 | delete_if_exists(benchfile) 187 | end 188 | 189 | def bench_fat_get_id 190 | filename = fixture(%i[i386 x86_64], "libhello.dylib") 191 | 192 | Benchmark.ips do |bm| 193 | bm.report("otool_fat_get_id") do 194 | libs = `otool -L #{filename}`.split("\n") 195 | libs.shift 196 | libs.shift[OTOOL_RX, 1] 197 | end 198 | 199 | bm.report("ruby_fat_get_id") do 200 | MachO.open(filename).dylib_id 201 | end 202 | 203 | bm.compare! 204 | end 205 | end 206 | 207 | def bench_fat_get_dylib 208 | filename = fixture(%i[i386 x86_64], "libhello.dylib") 209 | 210 | Benchmark.ips do |bm| 211 | bm.report("otool_fat_get_dylib") do 212 | libs = `otool -L #{filename}`.split("\n") 213 | libs.shift(2) 214 | libs.map! { |lib| lib[OTOOL_RX, 1] }.compact! 215 | end 216 | 217 | bm.report("ruby_fat_get_dylib") do 218 | MachO::Tools.dylibs(filename) 219 | end 220 | 221 | bm.compare! 222 | end 223 | end 224 | 225 | def bench_fat_set_id 226 | filename = fixture(%i[i386 x86_64], "libhello.dylib") 227 | benchfile = "#{filename}.bench" 228 | FileUtils.cp(filename, benchfile) 229 | i = 0 230 | 231 | Benchmark.ips do |bm| 232 | bm.report("int_fat_set_id") do 233 | `install_name_tool -id #{i += 1} #{benchfile}` 234 | end 235 | 236 | i = 0 237 | 238 | bm.report("ruby_fat_set_id") do 239 | MachO::Tools.change_dylib_id(benchfile, (i += 1).to_s) 240 | end 241 | 242 | bm.compare! 243 | end 244 | ensure 245 | delete_if_exists(benchfile) 246 | end 247 | 248 | def bench_fat_set_dylib 249 | filename = fixture(%i[i386 x86_64], "libhello.dylib") 250 | benchfile = "#{filename}.bench" 251 | FileUtils.cp(filename, benchfile) 252 | i = 0 253 | 254 | Benchmark.ips do |bm| 255 | MachO::Tools.change_install_name(benchfile, "/usr/lib/libSystem.B.dylib", "0") 256 | 257 | bm.report("int_fat_set_dylib") do 258 | `install_name_tool -change #{i} #{i += 1} #{benchfile}` 259 | end 260 | 261 | MachO::Tools.change_install_name(benchfile, i.to_s, "0") 262 | i = 0 263 | 264 | bm.report("ruby_fat_set_dylib") do 265 | MachO::Tools.change_install_name(benchfile, i.to_s, (i += 1).to_s) 266 | end 267 | 268 | bm.compare! 269 | end 270 | ensure 271 | delete_if_exists(benchfile) 272 | end 273 | 274 | def bench_fat_add_rpath 275 | filename = fixture(%i[i386 x86_64], "libhello.dylib") 276 | benchfile = "#{filename}.bench" 277 | i = 0 278 | 279 | Benchmark.ips do |bm| 280 | bm.report("int_fat_add_rpath") do 281 | FileUtils.cp(filename, benchfile) 282 | `install_name_tool -add_rpath #{i += 1} #{benchfile}` 283 | FileUtils.rm(benchfile) 284 | end 285 | 286 | bm.report("ruby_fat_add_rpath") do 287 | FileUtils.cp(filename, benchfile) 288 | MachO::Tools.add_rpath(benchfile, (i += 1).to_s) 289 | FileUtils.rm(benchfile) 290 | end 291 | 292 | bm.compare! 293 | end 294 | ensure 295 | delete_if_exists(benchfile) 296 | end 297 | 298 | def bench_fat_delete_rpath 299 | filename = fixture(%i[i386 x86_64], "hello.bin") 300 | benchfile = "#{filename}.bench" 301 | rpath = MachO.open(filename).rpaths.first 302 | 303 | Benchmark.ips do |bm| 304 | bm.report("int_fat_del_rpath") do 305 | FileUtils.cp(filename, benchfile) 306 | `install_name_tool -delete_rpath #{rpath} #{benchfile}` 307 | FileUtils.rm(benchfile) 308 | end 309 | 310 | bm.report("ruby_fat_del_rpath") do 311 | FileUtils.cp(filename, benchfile) 312 | MachO::Tools.delete_rpath(benchfile, rpath) 313 | FileUtils.rm(benchfile) 314 | end 315 | 316 | bm.compare! 317 | end 318 | ensure 319 | delete_if_exists(benchfile) 320 | end 321 | 322 | def bench_fat_change_rpath 323 | filename = fixture(%i[i386 x86_64], "hello.bin") 324 | benchfile = "#{filename}.bench" 325 | FileUtils.cp(filename, benchfile) 326 | rpath = MachO.open(filename).rpaths.first 327 | i = 0 328 | 329 | MachO::Tools.change_rpath(benchfile, rpath, i.to_s) 330 | 331 | Benchmark.ips do |bm| 332 | bm.report("int_fat_change_rpath") do 333 | `install_name_tool -rpath #{i} #{i += 1} #{benchfile}` 334 | end 335 | 336 | bm.report("ruby_fat_change_rpath") do 337 | MachO::Tools.change_rpath(benchfile, i.to_s, (i += 1).to_s) 338 | end 339 | 340 | bm.compare! 341 | end 342 | ensure 343 | delete_if_exists(benchfile) 344 | end 345 | end 346 | -------------------------------------------------------------------------------- /test/test_tools.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class MachOToolsTest < Minitest::Test 6 | include Helpers 7 | 8 | def test_dylibs 9 | dylibs = MachO::Tools.dylibs(fixture(:x86_64, "hello.bin")) 10 | 11 | assert dylibs 12 | assert_kind_of Array, dylibs 13 | 14 | dylibs.each do |dylib| 15 | assert dylib 16 | assert_kind_of String, dylib 17 | end 18 | end 19 | 20 | def test_dylibs_fat 21 | dylibs = MachO::Tools.dylibs(fixture(%i[i386 x86_64], "hello.bin")) 22 | 23 | assert dylibs 24 | assert_kind_of Array, dylibs 25 | 26 | dylibs.each do |dylib| 27 | assert dylib 28 | assert_kind_of String, dylib 29 | end 30 | end 31 | 32 | def test_change_dylib_id 33 | groups = SINGLE_ARCHES.map do |arch| 34 | ["libhello.dylib", "libhello_actual.dylib", "libhello_expected.dylib"].map do |fn| 35 | fixture(arch, fn) 36 | end 37 | end 38 | 39 | groups.each do |filename, actual, expected| 40 | FileUtils.cp filename, actual 41 | MachO::Tools.change_dylib_id(actual, "test") 42 | 43 | assert equal_sha1_hashes(actual, expected) 44 | 45 | act = MachO::MachOFile.new(actual) 46 | exp = MachO::MachOFile.new(expected) 47 | 48 | assert_equal exp.dylib_id, act.dylib_id 49 | end 50 | ensure 51 | groups.each do |_, actual, _| 52 | delete_if_exists(actual) 53 | end 54 | end 55 | 56 | def test_change_dylib_id_fat 57 | groups = FAT_ARCH_PAIRS.map do |arch| 58 | ["libhello.dylib", "libhello_actual.dylib", "libhello_expected.dylib"].map do |fn| 59 | fixture(arch, fn) 60 | end 61 | end 62 | 63 | groups.each do |filename, actual, expected| 64 | FileUtils.cp filename, actual 65 | MachO::Tools.change_dylib_id(actual, "test") 66 | 67 | assert equal_sha1_hashes(actual, expected) 68 | 69 | act = MachO::FatFile.new(actual) 70 | exp = MachO::FatFile.new(expected) 71 | 72 | assert_equal exp.dylib_id, act.dylib_id 73 | end 74 | ensure 75 | groups.each do |_, actual, _| 76 | delete_if_exists(actual) 77 | end 78 | end 79 | 80 | def test_change_install_name 81 | groups = SINGLE_ARCHES.map do |arch| 82 | ["hello.bin", "hello_actual.bin", "hello_expected.bin"].map do |fn| 83 | fixture(arch, fn) 84 | end 85 | end 86 | 87 | groups.each do |filename, actual, expected| 88 | FileUtils.cp filename, actual 89 | oldname = MachO::Tools.dylibs(actual).first 90 | MachO::Tools.change_install_name(actual, oldname, "test") 91 | 92 | assert equal_sha1_hashes(actual, expected) 93 | 94 | act = MachO::MachOFile.new(actual) 95 | exp = MachO::MachOFile.new(expected) 96 | 97 | assert_equal exp.linked_dylibs.first, act.linked_dylibs.first 98 | end 99 | ensure 100 | groups.each do |_, actual, _| 101 | delete_if_exists(actual) 102 | end 103 | end 104 | 105 | def test_change_install_name_fat 106 | groups = FAT_ARCH_PAIRS.map do |arch| 107 | ["hello.bin", "hello_actual.bin", "hello_expected.bin"].map do |fn| 108 | fixture(arch, fn) 109 | end 110 | end 111 | 112 | groups.each do |filename, actual, expected| 113 | FileUtils.cp filename, actual 114 | oldname = MachO::Tools.dylibs(actual).first 115 | MachO::Tools.change_install_name(actual, oldname, "test") 116 | 117 | assert equal_sha1_hashes(actual, expected) 118 | 119 | act = MachO::FatFile.new(actual) 120 | exp = MachO::FatFile.new(expected) 121 | 122 | assert_equal exp.linked_dylibs.first, act.linked_dylibs.first 123 | end 124 | ensure 125 | groups.each do |_, actual, _| 126 | delete_if_exists(actual) 127 | end 128 | end 129 | 130 | def test_change_rpath 131 | groups = SINGLE_ARCHES.map do |arch| 132 | ["hello.bin", "hello_actual.bin", "hello_rpath_expected.bin"].map do |fn| 133 | fixture(arch, fn) 134 | end 135 | end 136 | 137 | groups.each do |filename, actual, expected| 138 | FileUtils.cp filename, actual 139 | MachO::Tools.change_rpath(actual, "made_up_path", "/usr/lib") 140 | 141 | assert equal_sha1_hashes(actual, expected) 142 | 143 | file = MachO::MachOFile.new(filename) 144 | act = MachO::MachOFile.new(actual) 145 | exp = MachO::MachOFile.new(expected) 146 | 147 | assert_equal file.rpaths.size, act.rpaths.size 148 | assert_equal file.ncmds, act.ncmds 149 | assert_equal exp.rpaths.size, act.rpaths.size 150 | assert_equal exp.ncmds, act.ncmds 151 | 152 | assert_equal exp.rpaths.first, act.rpaths.first 153 | end 154 | ensure 155 | groups.each do |_, actual, _| 156 | delete_if_exists(actual) 157 | end 158 | end 159 | 160 | def test_change_rpath_fat 161 | groups = FAT_ARCH_PAIRS.map do |arch| 162 | ["hello.bin", "hello_actual.bin", "hello_rpath_expected.bin"].map do |fn| 163 | fixture(arch, fn) 164 | end 165 | end 166 | 167 | groups.each do |filename, actual, expected| 168 | FileUtils.cp filename, actual 169 | MachO::Tools.change_rpath(actual, "made_up_path", "/usr/lib") 170 | 171 | assert equal_sha1_hashes(actual, expected) 172 | 173 | file = MachO::FatFile.new(filename) 174 | act = MachO::FatFile.new(actual) 175 | exp = MachO::FatFile.new(expected) 176 | 177 | assert_equal file.rpaths.size, act.rpaths.size 178 | assert_equal exp.rpaths.size, act.rpaths.size 179 | 180 | assert_equal exp.rpaths.first, act.rpaths.first 181 | end 182 | ensure 183 | groups.each do |_, actual, _| 184 | delete_if_exists(actual) 185 | end 186 | end 187 | 188 | def test_add_rpath 189 | groups = SINGLE_ARCHES.map do |arch| 190 | ["hello.bin", "hello_actual.bin"].map do |fn| 191 | fixture(arch, fn) 192 | end 193 | end 194 | 195 | groups.each do |filename, actual| 196 | FileUtils.cp filename, actual 197 | MachO::Tools.add_rpath(actual, "/foo/bar/baz") 198 | 199 | original = MachO::MachOFile.new(filename) 200 | modified = MachO::MachOFile.new(actual) 201 | 202 | assert_operator modified.ncmds, :>, original.ncmds 203 | assert_operator modified.sizeofcmds, :>, original.sizeofcmds 204 | assert_operator modified.rpaths.size, :>, original.rpaths.size 205 | refute_includes original.rpaths, "/foo/bar/baz" 206 | assert_includes modified.rpaths, "/foo/bar/baz" 207 | end 208 | ensure 209 | groups.each do |_, actual| 210 | delete_if_exists(actual) 211 | end 212 | end 213 | 214 | def test_add_rpath_fat 215 | groups = FAT_ARCH_PAIRS.map do |arch| 216 | ["hello.bin", "hello_actual.bin"].map do |fn| 217 | fixture(arch, fn) 218 | end 219 | end 220 | 221 | groups.each do |filename, actual| 222 | FileUtils.cp filename, actual 223 | MachO::Tools.add_rpath(actual, "/foo/bar/baz") 224 | 225 | original = MachO::FatFile.new(filename) 226 | modified = MachO::FatFile.new(actual) 227 | 228 | refute_includes original.rpaths, "/foo/bar/baz" 229 | assert_includes modified.rpaths, "/foo/bar/baz" 230 | end 231 | ensure 232 | groups.each do |_, actual| 233 | delete_if_exists(actual) 234 | end 235 | end 236 | 237 | def test_delete_rpath 238 | groups = SINGLE_ARCHES.map do |arch| 239 | ["hello.bin", "hello_actual.bin"].map do |fn| 240 | fixture(arch, fn) 241 | end 242 | end 243 | 244 | groups.each do |filename, actual| 245 | FileUtils.cp filename, actual 246 | MachO::Tools.delete_rpath(actual, "made_up_path") 247 | 248 | original = MachO::MachOFile.new(filename) 249 | modified = MachO::MachOFile.new(actual) 250 | 251 | assert_operator modified.ncmds, :<, original.ncmds 252 | assert_operator modified.sizeofcmds, :<, original.sizeofcmds 253 | assert_operator modified.rpaths.size, :<, original.rpaths.size 254 | assert_includes original.rpaths, "made_up_path" 255 | refute_includes modified.rpaths, "made_up_path" 256 | end 257 | ensure 258 | groups.each do |_, actual| 259 | delete_if_exists(actual) 260 | end 261 | end 262 | 263 | def test_delete_rpath_fat 264 | groups = FAT_ARCH_PAIRS.map do |arch| 265 | ["hello.bin", "hello_actual.bin"].map do |fn| 266 | fixture(arch, fn) 267 | end 268 | end 269 | 270 | groups.each do |filename, actual| 271 | FileUtils.cp filename, actual 272 | MachO::Tools.delete_rpath(actual, "made_up_path") 273 | 274 | original = MachO::FatFile.new(filename) 275 | modified = MachO::FatFile.new(actual) 276 | 277 | assert_operator modified.rpaths.size, :<, original.rpaths.size 278 | assert_includes original.rpaths, "made_up_path" 279 | refute_includes modified.rpaths, "made_up_path" 280 | end 281 | ensure 282 | groups.each do |_, actual| 283 | delete_if_exists(actual) 284 | end 285 | end 286 | 287 | def test_merge_machos 288 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 289 | merged_filename = "merged_machos.bin" 290 | 291 | # merge a bunch of single-arch Mach-Os and save them as a universal 292 | MachO::Tools.merge_machos(merged_filename, *filenames) 293 | 294 | # ensure that we can load the merged machos 295 | file = MachO::FatFile.new(merged_filename) 296 | 297 | assert file 298 | assert_instance_of MachO::FatFile, file 299 | assert_equal filenames.size, file.machos.size 300 | ensure 301 | delete_if_exists(merged_filename) 302 | end 303 | 304 | def test_merge_machos_fat 305 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hello.bin") } 306 | merged_filename = "merged_universals.bin" 307 | 308 | # merge a bunch of universal Mach-Os and save them as one universal 309 | MachO::Tools.merge_machos(merged_filename, *filenames) 310 | 311 | # ensure that we can load the merged machos 312 | file = MachO::FatFile.new(merged_filename) 313 | 314 | assert file 315 | assert_instance_of MachO::FatFile, file 316 | ensure 317 | delete_if_exists(merged_filename) 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /lib/macho/structure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # A general purpose pseudo-structure. Described in detail in machostructure-dsl-docs.md. 5 | # @abstract 6 | class MachOStructure 7 | # Constants used for parsing MachOStructure fields 8 | module Fields 9 | # 1. All fields with empty strings and zeros aren't used 10 | # to calculate the format and sizeof variables. 11 | # 2. All fields with nil should provide those values manually 12 | # via the :size parameter. 13 | 14 | # association of field types to byte size 15 | # @api private 16 | BYTE_SIZE = { 17 | # Binary slices 18 | :string => nil, 19 | :null_padded_string => nil, 20 | :int32 => 4, 21 | :uint32 => 4, 22 | :uint64 => 8, 23 | # Classes 24 | :view => 0, 25 | :lcstr => 4, 26 | :two_level_hints_table => 0, 27 | :tool_entries => 4, 28 | }.freeze 29 | 30 | # association of field types with ruby format codes 31 | # Binary format codes can be found here: 32 | # https://docs.ruby-lang.org/en/2.6.0/String.html#method-i-unpack 33 | # 34 | # The equals sign is used to manually change endianness using 35 | # the Utils#specialize_format() method. 36 | # @api private 37 | FORMAT_CODE = { 38 | # Binary slices 39 | :string => "a", 40 | :null_padded_string => "Z", 41 | :int32 => "l=", 42 | :uint32 => "L=", 43 | :uint64 => "Q=", 44 | # Classes 45 | :view => "", 46 | :lcstr => "L=", 47 | :two_level_hints_table => "", 48 | :tool_entries => "L=", 49 | }.freeze 50 | 51 | # A list of classes that must get initialized 52 | # To add a new class append it here and add the init method to the def_class_reader method 53 | # @api private 54 | CLASSES_TO_INIT = %i[lcstr tool_entries two_level_hints_table].freeze 55 | 56 | # A list of fields that don't require arguments in the initializer 57 | # Used to calculate MachOStructure#min_args 58 | # @api private 59 | NO_ARG_REQUIRED = %i[two_level_hints_table].freeze 60 | end 61 | 62 | # map of field names to indices 63 | @field_idxs = {} 64 | 65 | # array of fields sizes 66 | @size_list = [] 67 | 68 | # array of field format codes 69 | @fmt_list = [] 70 | 71 | # minimum number of required arguments 72 | @min_args = 0 73 | 74 | # @param args [Array[Value]] list of field parameters 75 | def initialize(*args) 76 | raise ArgumentError, "Invalid number of arguments" if args.size < self.class.min_args 77 | 78 | @values = args 79 | end 80 | 81 | # @return [Hash] a hash representation of this {MachOStructure}. 82 | def to_h 83 | { 84 | "structure" => { 85 | "format" => self.class.format, 86 | "bytesize" => self.class.bytesize, 87 | }, 88 | } 89 | end 90 | 91 | class << self 92 | attr_reader :min_args 93 | 94 | # @param endianness [Symbol] either `:big` or `:little` 95 | # @param bin [String] the string to be unpacked into the new structure 96 | # @return [MachO::MachOStructure] the resulting structure 97 | # @api private 98 | def new_from_bin(endianness, bin) 99 | format = Utils.specialize_format(self.format, endianness) 100 | 101 | new(*bin.unpack(format)) 102 | end 103 | 104 | def format 105 | @format ||= @fmt_list.join 106 | end 107 | 108 | def bytesize 109 | @bytesize ||= @size_list.sum 110 | end 111 | 112 | private 113 | 114 | # @param subclass [Class] subclass type 115 | # @api private 116 | def inherited(subclass) # rubocop:disable Lint/MissingSuper 117 | # Clone all class instance variables 118 | field_idxs = @field_idxs.dup 119 | size_list = @size_list.dup 120 | fmt_list = @fmt_list.dup 121 | min_args = @min_args.dup 122 | 123 | # Add those values to the inheriting class 124 | subclass.class_eval do 125 | @field_idxs = field_idxs 126 | @size_list = size_list 127 | @fmt_list = fmt_list 128 | @min_args = min_args 129 | end 130 | end 131 | 132 | # @param name [Symbol] name of internal field 133 | # @param type [Symbol] type of field in terms of binary size 134 | # @param options [Hash] set of additional options 135 | # Expected options 136 | # :size [Integer] size in bytes 137 | # :mask [Integer] bitmask 138 | # :unpack [String] string format 139 | # :default [Value] default value 140 | # :to_s [Boolean] flag for generating #to_s 141 | # :endian [Symbol] optionally specify :big or :little endian 142 | # :padding [Symbol] optionally specify :null padding 143 | # @api private 144 | def field(name, type, **options) 145 | raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type) 146 | 147 | # Get field idx for size_list and fmt_list 148 | idx = if @field_idxs.key?(name) 149 | @field_idxs[name] 150 | else 151 | @min_args += 1 unless options.key?(:default) || Fields::NO_ARG_REQUIRED.include?(type) 152 | @field_idxs[name] = @field_idxs.size 153 | @size_list << nil 154 | @fmt_list << nil 155 | @field_idxs.size - 1 156 | end 157 | 158 | # Update string type if padding is specified 159 | type = :null_padded_string if type == :string && options[:padding] == :null 160 | 161 | # Add to size_list and fmt_list 162 | @size_list[idx] = Fields::BYTE_SIZE[type] || options[:size] 163 | @fmt_list[idx] = if options[:endian] 164 | Utils.specialize_format(Fields::FORMAT_CODE[type], options[:endian]) 165 | else 166 | Fields::FORMAT_CODE[type] 167 | end 168 | @fmt_list[idx] += options[:size].to_s if options.key?(:size) 169 | 170 | # Generate methods 171 | if Fields::CLASSES_TO_INIT.include?(type) 172 | def_class_reader(name, type, idx) 173 | elsif options.key?(:mask) 174 | def_mask_reader(name, idx, options[:mask]) 175 | elsif options.key?(:unpack) 176 | def_unpack_reader(name, idx, options[:unpack]) 177 | elsif options.key?(:default) 178 | def_default_reader(name, idx, options[:default]) 179 | else 180 | def_reader(name, idx) 181 | end 182 | 183 | def_to_s(name) if options[:to_s] 184 | end 185 | 186 | # 187 | # Method Generators 188 | # 189 | 190 | # Generates a reader method for classes that need to be initialized. 191 | # These classes are defined in the Fields::CLASSES_TO_INIT array. 192 | # @param name [Symbol] name of internal field 193 | # @param type [Symbol] type of field in terms of binary size 194 | # @param idx [Integer] the index of the field value in the @values array 195 | # @api private 196 | def def_class_reader(name, type, idx) 197 | case type 198 | when :lcstr 199 | define_method(name) do 200 | instance_variable_defined?("@#{name}") || 201 | instance_variable_set("@#{name}", LoadCommands::LoadCommand::LCStr.new(self, @values[idx])) 202 | 203 | instance_variable_get("@#{name}") 204 | end 205 | when :two_level_hints_table 206 | define_method(name) do 207 | instance_variable_defined?("@#{name}") || 208 | instance_variable_set("@#{name}", LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints)) 209 | 210 | instance_variable_get("@#{name}") 211 | end 212 | when :tool_entries 213 | define_method(name) do 214 | instance_variable_defined?("@#{name}") || 215 | instance_variable_set("@#{name}", LoadCommands::BuildVersionCommand::ToolEntries.new(view, @values[idx])) 216 | 217 | instance_variable_get("@#{name}") 218 | end 219 | end 220 | end 221 | 222 | # Generates a reader method for fields that need to be bitmasked. 223 | # @param name [Symbol] name of internal field 224 | # @param idx [Integer] the index of the field value in the @values array 225 | # @param mask [Integer] the bitmask 226 | # @api private 227 | def def_mask_reader(name, idx, mask) 228 | define_method(name) do 229 | instance_variable_defined?("@#{name}") || 230 | instance_variable_set("@#{name}", @values[idx] & ~mask) 231 | 232 | instance_variable_get("@#{name}") 233 | end 234 | end 235 | 236 | # Generates a reader method for fields that need further unpacking. 237 | # @param name [Symbol] name of internal field 238 | # @param idx [Integer] the index of the field value in the @values array 239 | # @param unpack [String] the format code used for further binary unpacking 240 | # @api private 241 | def def_unpack_reader(name, idx, unpack) 242 | define_method(name) do 243 | instance_variable_defined?("@#{name}") || 244 | instance_variable_set("@#{name}", @values[idx].unpack(unpack)) 245 | 246 | instance_variable_get("@#{name}") 247 | end 248 | end 249 | 250 | # Generates a reader method for fields that have default values. 251 | # @param name [Symbol] name of internal field 252 | # @param idx [Integer] the index of the field value in the @values array 253 | # @param default [Value] the default value 254 | # @api private 255 | def def_default_reader(name, idx, default) 256 | define_method(name) do 257 | instance_variable_defined?("@#{name}") || 258 | instance_variable_set("@#{name}", @values.size > idx ? @values[idx] : default) 259 | 260 | instance_variable_get("@#{name}") 261 | end 262 | end 263 | 264 | # Generates an attr_reader like method for a field. 265 | # @param name [Symbol] name of internal field 266 | # @param idx [Integer] the index of the field value in the @values array 267 | # @api private 268 | def def_reader(name, idx) 269 | define_method(name) do 270 | @values[idx] 271 | end 272 | end 273 | 274 | # Generates the to_s method based on the named field. 275 | # @param name [Symbol] name of the field 276 | # @api private 277 | def def_to_s(name) 278 | define_method(:to_s) do 279 | send(name).to_s 280 | end 281 | end 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /test/bin/llvm/LICENSE.txt: -------------------------------------------------------------------------------- 1 | ============================================================================== 2 | The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: 3 | ============================================================================== 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | 182 | APPENDIX: How to apply the Apache License to your work. 183 | 184 | To apply the Apache License to your work, attach the following 185 | boilerplate notice, with the fields enclosed by brackets "[]" 186 | replaced with your own identifying information. (Don't include 187 | the brackets!) The text should be enclosed in the appropriate 188 | comment syntax for the file format. We also recommend that a 189 | file or class name and description of purpose be included on the 190 | same "printed page" as the copyright notice for easier 191 | identification within third-party archives. 192 | 193 | Copyright [yyyy] [name of copyright owner] 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | 207 | 208 | ---- LLVM Exceptions to the Apache 2.0 License ---- 209 | 210 | As an exception, if, as a result of your compiling your source code, portions 211 | of this Software are embedded into an Object form of such source code, you 212 | may redistribute such embedded portions in such Object form without complying 213 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License. 214 | 215 | In addition, if you combine or link compiled forms of this Software with 216 | software that is licensed under the GPLv2 ("Combined Software") and if a 217 | court of competent jurisdiction determines that the patent provision (Section 218 | 3), the indemnity provision (Section 9) or other Section of the License 219 | conflicts with the conditions of the GPLv2, you may retroactively and 220 | prospectively choose to deem waived or otherwise exclude such Section(s) of 221 | the License, but only in their entirety and only with respect to the Combined 222 | Software. 223 | 224 | ============================================================================== 225 | Software from third parties included in the LLVM Project: 226 | ============================================================================== 227 | The LLVM Project contains third party software which is under different license 228 | terms. All such code will be identified clearly using at least one of two 229 | mechanisms: 230 | 1) It will be in a separate directory tree with its own `LICENSE.txt` or 231 | `LICENSE` file at the top containing the specific license and restrictions 232 | which apply to that software, or 233 | 2) It will contain specific license and restriction terms at the top of every 234 | file. 235 | 236 | ============================================================================== 237 | Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy): 238 | ============================================================================== 239 | University of Illinois/NCSA 240 | Open Source License 241 | 242 | Copyright (c) 2003-2019 University of Illinois at Urbana-Champaign. 243 | All rights reserved. 244 | 245 | Developed by: 246 | 247 | LLVM Team 248 | 249 | University of Illinois at Urbana-Champaign 250 | 251 | http://llvm.org 252 | 253 | Permission is hereby granted, free of charge, to any person obtaining a copy of 254 | this software and associated documentation files (the "Software"), to deal with 255 | the Software without restriction, including without limitation the rights to 256 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 257 | of the Software, and to permit persons to whom the Software is furnished to do 258 | so, subject to the following conditions: 259 | 260 | * Redistributions of source code must retain the above copyright notice, 261 | this list of conditions and the following disclaimers. 262 | 263 | * Redistributions in binary form must reproduce the above copyright notice, 264 | this list of conditions and the following disclaimers in the 265 | documentation and/or other materials provided with the distribution. 266 | 267 | * Neither the names of the LLVM Team, University of Illinois at 268 | Urbana-Champaign, nor the names of its contributors may be used to 269 | endorse or promote products derived from this Software without specific 270 | prior written permission. 271 | 272 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 273 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 274 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 275 | CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 276 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 277 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE 278 | SOFTWARE. 279 | 280 | -------------------------------------------------------------------------------- /lib/macho/fat_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module MachO 6 | # Represents a "Fat" file, which contains a header, a listing of available 7 | # architectures, and one or more Mach-O binaries. 8 | # @see https://en.wikipedia.org/wiki/Mach-O#Multi-architecture_binaries 9 | # @see MachOFile 10 | class FatFile 11 | extend Forwardable 12 | 13 | # @return [String] the filename loaded from, or nil if loaded from a binary string 14 | attr_accessor :filename 15 | 16 | # @return [Hash] any parser options that the instance was created with 17 | # @note Options specified in a {FatFile} trickle down into the internal {MachOFile}s. 18 | attr_reader :options 19 | 20 | # @return [Headers::FatHeader] the file's header 21 | attr_reader :header 22 | 23 | # @return [Array, Array] an array of Mach-O binaries 27 | attr_reader :machos 28 | 29 | # Creates a new FatFile from the given (single-arch) Mach-Os 30 | # @param machos [Array] the machos to combine 31 | # @param fat64 [Boolean] whether to use {Headers::FatArch64}s to represent each slice 32 | # @return [FatFile] a new FatFile containing the give machos 33 | # @raise [ArgumentError] if less than one Mach-O is given 34 | # @raise [FatArchOffsetOverflowError] if the Mach-Os are too big to be represented 35 | # in a 32-bit {Headers::FatArch} and `fat64` is `false`. 36 | def self.new_from_machos(*machos, fat64: false) 37 | raise ArgumentError, "expected at least one Mach-O" if machos.empty? 38 | 39 | fa_klass, magic = if fat64 40 | [Headers::FatArch64, Headers::FAT_MAGIC_64] 41 | else 42 | [Headers::FatArch, Headers::FAT_MAGIC] 43 | end 44 | 45 | # put the smaller alignments further forwards in fat macho, so that we do less padding 46 | machos = machos.sort_by(&:segment_alignment) 47 | 48 | bin = +"" 49 | 50 | bin << Headers::FatHeader.new(magic, machos.size).serialize 51 | offset = Headers::FatHeader.bytesize + (machos.size * fa_klass.bytesize) 52 | 53 | macho_pads = {} 54 | 55 | machos.each do |macho| 56 | macho_offset = Utils.round(offset, 2**macho.segment_alignment) 57 | 58 | raise FatArchOffsetOverflowError, macho_offset if !fat64 && macho_offset > ((2**32) - 1) 59 | 60 | macho_pads[macho] = Utils.padding_for(offset, 2**macho.segment_alignment) 61 | 62 | bin << fa_klass.new(macho.header.cputype, macho.header.cpusubtype, 63 | macho_offset, macho.serialize.bytesize, 64 | macho.segment_alignment).serialize 65 | 66 | offset += (macho.serialize.bytesize + macho_pads[macho]) 67 | end 68 | 69 | machos.each do |macho| # rubocop:disable Style/CombinableLoops 70 | bin << Utils.nullpad(macho_pads[macho]) 71 | bin << macho.serialize 72 | end 73 | 74 | new_from_bin(bin) 75 | end 76 | 77 | # Creates a new FatFile instance from a binary string. 78 | # @param bin [String] a binary string containing raw Mach-O data 79 | # @param opts [Hash] options to control the parser with 80 | # @note see {MachOFile#initialize} for currently valid options 81 | # @return [FatFile] a new FatFile 82 | def self.new_from_bin(bin, **opts) 83 | instance = allocate 84 | instance.initialize_from_bin(bin, opts) 85 | 86 | instance 87 | end 88 | 89 | # Creates a new FatFile from the given filename. 90 | # @param filename [String] the fat file to load from 91 | # @param opts [Hash] options to control the parser with 92 | # @note see {MachOFile#initialize} for currently valid options 93 | # @raise [ArgumentError] if the given file does not exist 94 | def initialize(filename, **opts) 95 | raise ArgumentError, "#{filename}: no such file" unless File.file?(filename) 96 | 97 | @filename = filename 98 | @options = opts 99 | @raw_data = File.binread(@filename) 100 | populate_fields 101 | end 102 | 103 | # Initializes a new FatFile instance from a binary string with the given options. 104 | # @see new_from_bin 105 | # @api private 106 | def initialize_from_bin(bin, opts) 107 | @filename = nil 108 | @options = opts 109 | @raw_data = bin 110 | populate_fields 111 | end 112 | 113 | # The file's raw fat data. 114 | # @return [String] the raw fat data 115 | def serialize 116 | @raw_data 117 | end 118 | 119 | # @!method object? 120 | # @return (see MachO::MachOFile#object?) 121 | # @!method executable? 122 | # @return (see MachO::MachOFile#executable?) 123 | # @!method fvmlib? 124 | # @return (see MachO::MachOFile#fvmlib?) 125 | # @!method core? 126 | # @return (see MachO::MachOFile#core?) 127 | # @!method preload? 128 | # @return (see MachO::MachOFile#preload?) 129 | # @!method dylib? 130 | # @return (see MachO::MachOFile#dylib?) 131 | # @!method dylinker? 132 | # @return (see MachO::MachOFile#dylinker?) 133 | # @!method bundle? 134 | # @return (see MachO::MachOFile#bundle?) 135 | # @!method dsym? 136 | # @return (see MachO::MachOFile#dsym?) 137 | # @!method kext? 138 | # @return (see MachO::MachOFile#kext?) 139 | # @!method filetype 140 | # @return (see MachO::MachOFile#filetype) 141 | # @!method dylib_id 142 | # @return (see MachO::MachOFile#dylib_id) 143 | def_delegators :canonical_macho, :object?, :executable?, :fvmlib?, 144 | :core?, :preload?, :dylib?, :dylinker?, :bundle?, 145 | :dsym?, :kext?, :filetype, :dylib_id 146 | 147 | # @!method magic 148 | # @return (see MachO::Headers::FatHeader#magic) 149 | def_delegators :header, :magic 150 | 151 | # @return [String] a string representation of the file's magic number 152 | def magic_string 153 | Headers::MH_MAGICS[magic] 154 | end 155 | 156 | # Populate the instance's fields with the raw Fat Mach-O data. 157 | # @return [void] 158 | # @note This method is public, but should (almost) never need to be called. 159 | def populate_fields 160 | @header = populate_fat_header 161 | @fat_archs = populate_fat_archs 162 | @machos = populate_machos 163 | end 164 | 165 | # All load commands responsible for loading dylibs in the file's Mach-O's. 166 | # @return [Array] an array of DylibCommands 167 | def dylib_load_commands 168 | machos.map(&:dylib_load_commands).flatten 169 | end 170 | 171 | # Changes the file's dylib ID to `new_id`. If the file is not a dylib, 172 | # does nothing. 173 | # @example 174 | # file.change_dylib_id('libFoo.dylib') 175 | # @param new_id [String] the new dylib ID 176 | # @param options [Hash] 177 | # @option options [Boolean] :strict (true) if true, fail if one slice fails. 178 | # if false, fail only if all slices fail. 179 | # @return [void] 180 | # @raise [ArgumentError] if `new_id` is not a String 181 | # @see MachOFile#linked_dylibs 182 | def change_dylib_id(new_id, options = {}) 183 | raise ArgumentError, "argument must be a String" unless new_id.is_a?(String) 184 | return unless machos.all?(&:dylib?) 185 | 186 | each_macho(options) do |macho| 187 | macho.change_dylib_id(new_id, options) 188 | end 189 | 190 | repopulate_raw_machos 191 | end 192 | 193 | alias dylib_id= change_dylib_id 194 | 195 | # All shared libraries linked to the file's Mach-Os. 196 | # @return [Array] an array of all shared libraries 197 | # @see MachOFile#linked_dylibs 198 | def linked_dylibs 199 | # Individual architectures in a fat binary can link to different subsets 200 | # of libraries, but at this point we want to have the full picture, i.e. 201 | # the union of all libraries used by all architectures. 202 | machos.map(&:linked_dylibs).flatten.uniq 203 | end 204 | 205 | # Changes all dependent shared library install names from `old_name` to 206 | # `new_name`. In a fat file, this changes install names in all internal 207 | # Mach-Os. 208 | # @example 209 | # file.change_install_name('/usr/lib/libFoo.dylib', '/usr/lib/libBar.dylib') 210 | # @param old_name [String] the shared library name being changed 211 | # @param new_name [String] the new name 212 | # @param options [Hash] 213 | # @option options [Boolean] :strict (true) if true, fail if one slice fails. 214 | # if false, fail only if all slices fail. 215 | # @return [void] 216 | # @see MachOFile#change_install_name 217 | def change_install_name(old_name, new_name, options = {}) 218 | each_macho(options) do |macho| 219 | macho.change_install_name(old_name, new_name, options) 220 | end 221 | 222 | repopulate_raw_machos 223 | end 224 | 225 | alias change_dylib change_install_name 226 | 227 | # All runtime paths associated with the file's Mach-Os. 228 | # @return [Array] an array of all runtime paths 229 | # @see MachOFile#rpaths 230 | def rpaths 231 | # Can individual architectures have different runtime paths? 232 | machos.map(&:rpaths).flatten.uniq 233 | end 234 | 235 | # Change the runtime path `old_path` to `new_path` in the file's Mach-Os. 236 | # @param old_path [String] the old runtime path 237 | # @param new_path [String] the new runtime path 238 | # @param options [Hash] 239 | # @option options [Boolean] :strict (true) if true, fail if one slice fails. 240 | # if false, fail only if all slices fail. 241 | # @option options [Boolean] :uniq (false) for each slice: if true, change 242 | # each rpath simultaneously. 243 | # @return [void] 244 | # @see MachOFile#change_rpath 245 | def change_rpath(old_path, new_path, options = {}) 246 | each_macho(options) do |macho| 247 | macho.change_rpath(old_path, new_path, options) 248 | end 249 | 250 | repopulate_raw_machos 251 | end 252 | 253 | # Add the given runtime path to the file's Mach-Os. 254 | # @param path [String] the new runtime path 255 | # @param options [Hash] 256 | # @option options [Boolean] :strict (true) if true, fail if one slice fails. 257 | # if false, fail only if all slices fail. 258 | # @return [void] 259 | # @see MachOFile#add_rpath 260 | def add_rpath(path, options = {}) 261 | each_macho(options) do |macho| 262 | macho.add_rpath(path, options) 263 | end 264 | 265 | repopulate_raw_machos 266 | end 267 | 268 | # Delete the given runtime path from the file's Mach-Os. 269 | # @param path [String] the runtime path to delete 270 | # @param options [Hash] 271 | # @option options [Boolean] :strict (true) if true, fail if one slice fails. 272 | # if false, fail only if all slices fail. 273 | # @option options [Boolean] :uniq (false) for each slice: if true, delete 274 | # only the first runtime path that matches. if false, delete all duplicate 275 | # paths that match. 276 | # @return void 277 | # @see MachOFile#delete_rpath 278 | def delete_rpath(path, options = {}) 279 | each_macho(options) do |macho| 280 | macho.delete_rpath(path, options) 281 | end 282 | 283 | repopulate_raw_machos 284 | end 285 | 286 | # Extract a Mach-O with the given CPU type from the file. 287 | # @example 288 | # file.extract(:i386) # => MachO::MachOFile 289 | # @param cputype [Symbol] the CPU type of the Mach-O being extracted 290 | # @return [MachOFile, nil] the extracted Mach-O or nil if no Mach-O has the given CPU type 291 | def extract(cputype) 292 | machos.select { |macho| macho.cputype == cputype }.first 293 | end 294 | 295 | # Write all (fat) data to the given filename. 296 | # @param filename [String] the file to write to 297 | # @return [void] 298 | def write(filename) 299 | File.binwrite(filename, @raw_data) 300 | end 301 | 302 | # Write all (fat) data to the file used to initialize the instance. 303 | # @return [void] 304 | # @raise [MachOError] if the instance was initialized without a file 305 | # @note Overwrites all data in the file! 306 | def write! 307 | raise MachOError, "no initial file to write to" if filename.nil? 308 | 309 | File.binwrite(@filename, @raw_data) 310 | end 311 | 312 | # @return [Hash] a hash representation of this {FatFile} 313 | def to_h 314 | { 315 | "header" => header.to_h, 316 | "fat_archs" => fat_archs.map(&:to_h), 317 | "machos" => machos.map(&:to_h), 318 | } 319 | end 320 | 321 | private 322 | 323 | # Obtain the fat header from raw file data. 324 | # @return [Headers::FatHeader] the fat header 325 | # @raise [TruncatedFileError] if the file is too small to have a 326 | # valid header 327 | # @raise [MagicError] if the magic is not valid Mach-O magic 328 | # @raise [MachOBinaryError] if the magic is for a non-fat Mach-O file 329 | # @raise [JavaClassFileError] if the file is a Java classfile 330 | # @raise [ZeroArchitectureError] if the file has no internal slices 331 | # (i.e., nfat_arch == 0) and the permissive option is not set 332 | # @api private 333 | def populate_fat_header 334 | # the smallest fat Mach-O header is 8 bytes 335 | raise TruncatedFileError if @raw_data.size < 8 336 | 337 | fh = Headers::FatHeader.new_from_bin(:big, @raw_data[0, Headers::FatHeader.bytesize]) 338 | 339 | raise MagicError, fh.magic unless Utils.magic?(fh.magic) 340 | raise MachOBinaryError unless Utils.fat_magic?(fh.magic) 341 | 342 | # Rationale: Java classfiles have the same magic as big-endian fat 343 | # Mach-Os. Classfiles encode their version at the same offset as 344 | # `nfat_arch` and the lowest version number is 43, so we error out 345 | # if a file claims to have over 30 internal architectures. It's 346 | # technically possible for a fat Mach-O to have over 30 architectures, 347 | # but this is extremely unlikely and in practice distinguishes the two 348 | # formats. 349 | raise JavaClassFileError if fh.nfat_arch > 30 350 | 351 | # Rationale: return an error if the file has no internal slices. 352 | raise ZeroArchitectureError if fh.nfat_arch.zero? 353 | 354 | fh 355 | end 356 | 357 | # Obtain an array of fat architectures from raw file data. 358 | # @return [Array] an array of fat architectures 359 | # @api private 360 | def populate_fat_archs 361 | archs = [] 362 | 363 | fa_klass = Utils.fat_magic32?(header.magic) ? Headers::FatArch : Headers::FatArch64 364 | fa_off = Headers::FatHeader.bytesize 365 | fa_len = fa_klass.bytesize 366 | 367 | header.nfat_arch.times do |i| 368 | archs << fa_klass.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len]) 369 | end 370 | 371 | archs 372 | end 373 | 374 | # Obtain an array of Mach-O blobs from raw file data. 375 | # @return [Array] an array of Mach-Os 376 | # @api private 377 | def populate_machos 378 | machos = [] 379 | 380 | fat_archs.each do |arch| 381 | machos << MachOFile.new_from_bin(@raw_data[arch.offset, arch.size], **options) 382 | 383 | # Make sure that each fat_arch and internal slice. 384 | # contain matching cputypes and cpusubtypes 385 | next if machos.last.header.cputype == arch.cputype && 386 | machos.last.header.cpusubtype == arch.cpusubtype 387 | 388 | raise CPUTypeMismatchError.new(arch.cputype, arch.cpusubtype, machos.last.header.cputype, machos.last.header.cpusubtype) 389 | end 390 | 391 | machos 392 | end 393 | 394 | # Repopulate the raw Mach-O data with each internal Mach-O object. 395 | # @return [void] 396 | # @api private 397 | def repopulate_raw_machos 398 | machos.each_with_index do |macho, i| 399 | arch = fat_archs[i] 400 | 401 | @raw_data[arch.offset, arch.size] = macho.serialize 402 | end 403 | end 404 | 405 | # Yield each Mach-O object in the file, rescuing and accumulating errors. 406 | # @param options [Hash] 407 | # @option options [Boolean] :strict (true) whether or not to fail loudly 408 | # with an exception if at least one Mach-O raises an exception. If false, 409 | # only raises an exception if *all* Mach-Os raise exceptions. 410 | # @raise [RecoverableModificationError] under the conditions of 411 | # the `:strict` option above. 412 | # @api private 413 | def each_macho(options = {}) 414 | strict = options.fetch(:strict, true) 415 | errors = [] 416 | 417 | machos.each_with_index do |macho, index| 418 | yield macho 419 | rescue RecoverableModificationError => e 420 | e.macho_slice = index 421 | 422 | # Strict mode: Immediately re-raise. Otherwise: Retain, check later. 423 | raise e if strict 424 | 425 | errors << e 426 | end 427 | 428 | # Non-strict mode: Raise first error if *all* Mach-O slices failed. 429 | raise errors.first if errors.size == machos.size 430 | end 431 | 432 | # Return a single-arch Mach-O that represents this fat Mach-O for purposes 433 | # of delegation. 434 | # @return [MachOFile] the Mach-O file 435 | # @api private 436 | def canonical_macho 437 | machos.first 438 | end 439 | end 440 | end 441 | -------------------------------------------------------------------------------- /test/test_fat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class FatFileTest < Minitest::Test 6 | include Helpers 7 | 8 | def test_nonexistent_file 9 | assert_raises ArgumentError do 10 | MachO::FatFile.new("/this/is/a/file/that/cannot/possibly/exist") 11 | end 12 | end 13 | 14 | def test_empty_file 15 | tempfile_with_data("empty_file", "") do |empty_file| 16 | assert_raises MachO::TruncatedFileError do 17 | MachO::FatFile.new(empty_file.path) 18 | end 19 | end 20 | end 21 | 22 | def test_truncated_file 23 | tempfile_with_data("truncated_file", "\xCA\xFE\xBA\xBE\x00\x00") do |truncated_file| 24 | assert_raises MachO::TruncatedFileError do 25 | MachO::FatFile.new(truncated_file.path) 26 | end 27 | end 28 | end 29 | 30 | def test_java_classfile 31 | blob = "\xCA\xFE\xBA\xBE\x00\x00\x00\x33therestofthisfileisnotarealjavaclassfile" 32 | tempfile_with_data("fake_java_class_file", blob) do |fake_java_class_file| 33 | assert_raises MachO::JavaClassFileError do 34 | MachO::FatFile.new(fake_java_class_file.path) 35 | end 36 | end 37 | end 38 | 39 | def test_zero_arch_file 40 | assert_raises MachO::ZeroArchitectureError do 41 | MachO::FatFile.new("test/bin/llvm/macho-invalid-fat-header") 42 | end 43 | end 44 | 45 | def test_mismatch_cpu_arch_file 46 | assert_raises MachO::CPUTypeMismatchError do 47 | MachO::FatFile.new("test/bin/llvm/macho-invalid-fat_cputype") 48 | end 49 | end 50 | 51 | def test_fat_header 52 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hello.bin") } 53 | 54 | filenames.each do |fn| 55 | file = MachO::FatFile.new(fn) 56 | 57 | header = file.header 58 | 59 | assert header 60 | assert_kind_of MachO::Headers::FatHeader, header 61 | assert_kind_of Integer, header.magic 62 | assert_kind_of Integer, header.nfat_arch 63 | end 64 | end 65 | 66 | def test_fat_archs 67 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "libhello.dylib") } 68 | 69 | filenames.each do |fn| 70 | file = MachO::FatFile.new(fn) 71 | archs = file.fat_archs 72 | 73 | assert archs 74 | assert_kind_of Array, archs 75 | 76 | archs.each do |arch| 77 | assert arch 78 | assert_kind_of MachO::Headers::FatArch, arch 79 | assert_kind_of Integer, arch.cputype 80 | assert_kind_of Integer, arch.cpusubtype 81 | assert_kind_of Integer, arch.offset 82 | assert_kind_of Integer, arch.size 83 | assert_kind_of Integer, arch.align 84 | end 85 | end 86 | end 87 | 88 | def test_machos 89 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hellobundle.so") } 90 | 91 | filenames.each do |fn| 92 | file = MachO::FatFile.new(fn) 93 | 94 | machos = file.machos 95 | assert machos 96 | assert_kind_of Array, machos 97 | 98 | machos.each do |macho| 99 | assert macho 100 | assert_kind_of MachO::MachOFile, macho 101 | 102 | assert macho.serialize 103 | assert_kind_of String, macho.serialize 104 | end 105 | end 106 | end 107 | 108 | def test_file 109 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hello.bin") } 110 | 111 | filenames.each do |fn| 112 | file = MachO::FatFile.new(fn) 113 | 114 | assert file.serialize 115 | assert_kind_of String, file.serialize 116 | 117 | assert_kind_of Integer, file.magic 118 | assert_kind_of String, file.magic_string 119 | assert_kind_of Symbol, file.filetype 120 | end 121 | end 122 | 123 | def test_object 124 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hello.o") } 125 | 126 | filenames.each do |fn| 127 | file = MachO::FatFile.new(fn) 128 | 129 | assert file.object? 130 | filechecks(:object?).each do |check| 131 | refute file.send(check) 132 | end 133 | 134 | assert_equal :object, file.filetype 135 | end 136 | end 137 | 138 | def test_executable 139 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hello.bin") } 140 | 141 | filenames.each do |fn| 142 | file = MachO::FatFile.new(fn) 143 | 144 | assert file.executable? 145 | filechecks(:executable?).each do |check| 146 | refute file.send(check) 147 | end 148 | 149 | assert_equal :execute, file.filetype 150 | end 151 | end 152 | 153 | def test_dylib 154 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "libhello.dylib") } 155 | 156 | filenames.each do |fn| 157 | file = MachO::FatFile.new(fn) 158 | 159 | assert file.dylib? 160 | filechecks(:dylib?).each do |check| 161 | refute file.send(check) 162 | end 163 | 164 | assert_equal :dylib, file.filetype 165 | end 166 | end 167 | 168 | def test_extra_dylib 169 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "libextrahello.dylib") } 170 | unusual_dylib_lcs = %i[ 171 | LC_LOAD_UPWARD_DYLIB 172 | LC_LAZY_LOAD_DYLIB 173 | LC_LOAD_WEAK_DYLIB 174 | LC_REEXPORT_DYLIB 175 | ] 176 | 177 | filenames.each do |fn| 178 | file = MachO::FatFile.new(fn) 179 | 180 | assert file.dylib? 181 | 182 | file.machos.each do |macho| 183 | # make sure we can read more unusual dylib load commands 184 | unusual_dylib_lcs.each do |cmdname| 185 | lc = macho[cmdname].first 186 | 187 | # PPC and x86-family binaries don't have the same dylib LCs, so ignore 188 | # the ones that don't exist 189 | # https://github.com/Homebrew/ruby-macho/pull/24#issuecomment-226287121 190 | next unless lc 191 | 192 | assert_kind_of MachO::LoadCommands::DylibCommand, lc 193 | 194 | dylib_name = lc.name 195 | 196 | assert dylib_name 197 | assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, dylib_name 198 | end 199 | end 200 | end 201 | 202 | # TODO: figure out why we can't make dylibs with LC_LAZY_LOAD_DYLIB commands 203 | # @see https://github.com/Homebrew/ruby-macho/issues/6 204 | end 205 | 206 | def test_bundle 207 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hellobundle.so") } 208 | 209 | filenames.each do |fn| 210 | file = MachO::FatFile.new(fn) 211 | 212 | assert file.bundle? 213 | filechecks(:bundle?).each do |check| 214 | refute file.send(check) 215 | end 216 | 217 | assert_equal :bundle, file.filetype 218 | end 219 | end 220 | 221 | def test_extract_macho 222 | groups = FAT_ARCH_PAIRS.map do |arch| 223 | ["hello.bin", "extracted_macho1", "extracted_macho2"].map do |fn| 224 | fixture(arch, fn) 225 | end 226 | end 227 | 228 | groups.each do |filename, extract1, extract2| 229 | file = MachO::FatFile.new(filename) 230 | 231 | assert file.machos.size == 2 232 | 233 | macho1 = file.extract(file.machos[0].cputype) 234 | macho2 = file.extract(file.machos[1].cputype) 235 | not_real = file.extract(:nonexistent) 236 | 237 | assert macho1 238 | assert macho2 239 | assert_nil not_real 240 | 241 | assert_equal file.machos[0].serialize, macho1.serialize 242 | assert_equal file.machos[1].serialize, macho2.serialize 243 | 244 | # write the extracted mach-os to disk 245 | macho1.write(extract1) 246 | macho2.write(extract2) 247 | 248 | # load them back to ensure they're intact/uncorrupted 249 | mfile1 = MachO::MachOFile.new(extract1) 250 | mfile2 = MachO::MachOFile.new(extract2) 251 | 252 | assert_equal file.machos[0].serialize, mfile1.serialize 253 | assert_equal file.machos[1].serialize, mfile2.serialize 254 | end 255 | ensure 256 | groups.each do |_, extract1, extract2| 257 | delete_if_exists(extract1) 258 | delete_if_exists(extract2) 259 | end 260 | end 261 | 262 | def test_new_from_machos 263 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 264 | machos = filenames.map { |f| MachO::MachOFile.new(f) } 265 | 266 | file = MachO::FatFile.new_from_machos(*machos) 267 | 268 | assert file 269 | assert_instance_of MachO::FatFile, file 270 | 271 | # the number of machos inside the new fat file should be the number 272 | # of individual machos we gave it 273 | assert machos.size, file.machos.size 274 | 275 | # the order of machos within the fat file should be preserved 276 | machos.each_with_index do |macho, i| 277 | assert_equal macho.cputype, file.machos[i].cputype 278 | end 279 | 280 | # we should be able to dump the newly created fat file 281 | file.write("merged_machos.bin") 282 | 283 | # ...and load it back as a new object without errors 284 | file = MachO::FatFile.new("merged_machos.bin") 285 | 286 | assert file 287 | ensure 288 | delete_if_exists("merged_machos.bin") 289 | end 290 | 291 | def test_change_dylib_id 292 | groups = FAT_ARCH_PAIRS.map do |arch| 293 | ["libhello.dylib", "libhello_actual.dylib", "libhello_expected.dylib"].map do |fn| 294 | fixture(arch, fn) 295 | end 296 | end 297 | 298 | groups.each do |filename, actual, expected| 299 | file = MachO::FatFile.new(filename) 300 | 301 | # changing the dylib id should work 302 | old_id = file.dylib_id 303 | file.dylib_id = "testing" 304 | assert_equal "testing", file.dylib_id 305 | 306 | # change it back within the same instance 307 | file.dylib_id = old_id 308 | assert_equal old_id, file.dylib_id 309 | 310 | really_big_id = "x" * 4096 311 | 312 | # test failsafe for excessively large IDs (w/ no special linking) 313 | assert_raises MachO::HeaderPadError do 314 | file.dylib_id = really_big_id 315 | end 316 | 317 | file.dylib_id = "test" 318 | 319 | file.write(actual) 320 | 321 | assert equal_sha1_hashes(actual, expected) 322 | 323 | act = MachO::FatFile.new(actual) 324 | exp = MachO::FatFile.new(expected) 325 | 326 | assert_equal exp.dylib_id, act.dylib_id 327 | end 328 | ensure 329 | groups.each do |_, actual, _| 330 | delete_if_exists(actual) 331 | end 332 | end 333 | 334 | def test_change_install_name 335 | groups = FAT_ARCH_PAIRS.map do |arch| 336 | ["hello.bin", "hello_actual.bin", "hello_expected.bin"].map do |fn| 337 | fixture(arch, fn) 338 | end 339 | end 340 | 341 | groups.each do |filename, actual, expected| 342 | file = MachO::FatFile.new(filename) 343 | 344 | dylibs = file.linked_dylibs 345 | 346 | # there should be at least one dylib linked to the binary 347 | refute_empty dylibs 348 | 349 | file.change_install_name(dylibs.first, "test") 350 | new_dylibs = file.linked_dylibs 351 | 352 | # the new dylib name should reflect the changes we've made 353 | assert_equal "test", new_dylibs.first 354 | refute_equal dylibs.first, new_dylibs.first 355 | 356 | file.write(actual) 357 | 358 | assert equal_sha1_hashes(actual, expected) 359 | 360 | act = MachO::FatFile.new(actual) 361 | exp = MachO::FatFile.new(expected) 362 | 363 | assert_equal exp.linked_dylibs.first, act.linked_dylibs.first 364 | end 365 | ensure 366 | groups.each do |_, actual, _| 367 | delete_if_exists(actual) 368 | end 369 | end 370 | 371 | def test_get_rpaths 372 | groups = FAT_ARCH_PAIRS.map do |arch| 373 | ["", "_actual", "_expected"].map do |fn| 374 | fixture(arch, "hello#{fn}.bin") 375 | end 376 | end 377 | 378 | groups.each do |filename, _, _| 379 | file = MachO::FatFile.new(filename) 380 | rpaths = file.rpaths 381 | 382 | assert_kind_of Array, rpaths 383 | assert_kind_of String, rpaths.first 384 | assert_equal "made_up_path", rpaths.first 385 | end 386 | end 387 | 388 | def test_change_rpath 389 | groups = FAT_ARCH_PAIRS.map do |arch| 390 | ["", "_rpath_actual", "_rpath_expected"].map do |fn| 391 | fixture(arch, "hello#{fn}.bin") 392 | end 393 | end 394 | 395 | groups.each do |filename, actual, expected| 396 | file = MachO::FatFile.new(filename) 397 | rpaths = file.rpaths 398 | 399 | # there should be at least one rpath in each binary 400 | refute_empty rpaths 401 | 402 | file.change_rpath(rpaths.first, "/usr/lib") 403 | new_rpaths = file.rpaths 404 | 405 | # the new rpath should reflect the changes we've made 406 | assert_equal "/usr/lib", new_rpaths.first 407 | refute_empty rpaths.first, new_rpaths.first 408 | 409 | file.write(actual) 410 | 411 | assert equal_sha1_hashes(actual, expected) 412 | 413 | act = MachO::FatFile.new(actual) 414 | exp = MachO::FatFile.new(expected) 415 | 416 | assert_equal file.rpaths.size, act.rpaths.size 417 | assert_equal exp.rpaths.size, act.rpaths.size 418 | 419 | assert_equal exp.rpaths.first, act.rpaths.first 420 | end 421 | ensure 422 | groups.each do |_, actual, _| 423 | delete_if_exists(actual) 424 | end 425 | end 426 | 427 | def test_delete_rpath 428 | groups = FAT_ARCH_PAIRS.map do |arch| 429 | ["hello.bin", "hello_actual.bin"].map do |fn| 430 | fixture(arch, fn) 431 | end 432 | end 433 | 434 | groups.each do |filename, actual| 435 | file = MachO::FatFile.new(filename) 436 | 437 | refute_empty file.rpaths 438 | orig_npaths = file.rpaths.size 439 | 440 | file.delete_rpath(file.rpaths.first) 441 | assert_operator file.rpaths.size, :<, orig_npaths 442 | 443 | file.write(actual) 444 | # ensure we can actually re-load and parse the modified file 445 | modified = MachO::FatFile.new(actual) 446 | 447 | assert_equal file.serialize.size, modified.serialize.size 448 | assert_equal file.rpaths.size, modified.rpaths.size 449 | assert_operator modified.rpaths.size, :<, orig_npaths 450 | end 451 | ensure 452 | groups.each do |_, actual| 453 | delete_if_exists(actual) 454 | end 455 | end 456 | 457 | def test_add_rpath 458 | groups = FAT_ARCH_PAIRS.map do |arch| 459 | ["hello.bin", "hello_actual.bin"].map do |fn| 460 | fixture(arch, fn) 461 | end 462 | end 463 | 464 | groups.each do |filename, actual| 465 | file = MachO::FatFile.new(filename) 466 | 467 | orig_npaths = file.rpaths.size 468 | 469 | file.add_rpath("/foo/bar/baz") 470 | assert_operator file.rpaths.size, :>, orig_npaths 471 | assert_includes file.rpaths, "/foo/bar/baz" 472 | 473 | file.write(actual) 474 | # ensure we can actually re-load and parse the modified file 475 | modified = MachO::FatFile.new(actual) 476 | 477 | assert_equal file.serialize.size, modified.serialize.size 478 | assert_equal file.rpaths.size, modified.rpaths.size 479 | assert_operator modified.rpaths.size, :>, orig_npaths 480 | assert_includes modified.rpaths, "/foo/bar/baz" 481 | end 482 | ensure 483 | groups.each do |_, actual| 484 | delete_if_exists(actual) 485 | end 486 | end 487 | 488 | def test_inconsistent_slices 489 | filename = fixture(%i[i386 x86_64], "libinconsistent.dylib") 490 | 491 | file = MachO::FatFile.new(filename) 492 | 493 | # the individual slices should have different sets of dylibs 494 | refute_equal file.machos[0].linked_dylibs, file.machos[1].linked_dylibs 495 | 496 | # modifications are strict by default 497 | assert_raises MachO::DylibUnknownError do 498 | # libz only exists in one of the slices 499 | file.change_install_name("/usr/lib/libz.1.dylib", "foo") 500 | end 501 | 502 | # completely incorrect modifications still fail with nonstrict 503 | assert_raises MachO::DylibUnknownError do 504 | # foo exists in none of the slices 505 | file.change_install_name("foo", "bar", :strict => false) 506 | end 507 | 508 | # with nonstrict, valid modifications should succeed for the right slice(s) 509 | file.change_install_name("/usr/lib/libz.1.dylib", "foo", :strict => false) 510 | 511 | # ...but not all slices will have the modified dylib 512 | refute(file.machos.all? { |m| m.linked_dylibs.include?("foo") }) 513 | 514 | # ...but at least one will 515 | assert(file.machos.any? { |m| m.linked_dylibs.include?("foo") }) 516 | end 517 | 518 | def test_dylib_load_commands 519 | filenames = FAT_ARCH_PAIRS.map { |a| fixture(a, "hello.bin") } 520 | 521 | filenames.each do |filename| 522 | file = MachO::FatFile.new(filename) 523 | 524 | assert_instance_of Array, file.dylib_load_commands 525 | 526 | file.dylib_load_commands.each do |lc| 527 | assert_kind_of MachO::LoadCommands::DylibCommand, lc 528 | end 529 | end 530 | end 531 | 532 | def test_fail_loading_thin 533 | filename = fixture("x86_64", "libhello.dylib") 534 | 535 | ex = assert_raises(MachO::MachOBinaryError) do 536 | MachO::FatFile.new_from_bin File.read(filename) 537 | end 538 | 539 | assert_match(/must be/, ex.inspect) 540 | end 541 | 542 | def test_to_h 543 | filename = fixture(%i[i386 x86_64], "hello.bin") 544 | file = MachO::FatFile.new(filename) 545 | hsh = file.to_h 546 | 547 | # the hash representation of a FatFile should have at least a header, fat_archs, and machos 548 | assert_kind_of Hash, hsh["header"] 549 | assert_kind_of Array, hsh["fat_archs"] 550 | assert_kind_of Array, hsh["machos"] 551 | 552 | header_fields = %w[ 553 | magic 554 | nfat_arch 555 | ] 556 | 557 | # fields in the header should be the same as in the hash representation 558 | header_fields.each do |field| 559 | assert_equal file.header.send(field), hsh["header"][field] 560 | end 561 | 562 | # additionally, symbol keys in the hash representation should correspond 563 | # to looked-up values in the header 564 | assert_equal MachO::Headers::MH_MAGICS[file.header.magic], hsh["header"]["magic_sym"] 565 | 566 | fat_arch_fields = %w[ 567 | cputype 568 | cpusubtype 569 | offset 570 | size 571 | align 572 | ] 573 | 574 | # fields in each FatArch should be the same as in the hash representation 575 | # 576 | # additionally, symbol keys in the hash representation of each FatArch should 577 | # correspond to looked up values 578 | file.fat_archs.zip(hsh["fat_archs"]).each do |fa, fa_hsh| 579 | fat_arch_fields.each do |field| 580 | assert_equal fa.send(field), fa_hsh[field] 581 | end 582 | 583 | assert_equal MachO::Headers::CPU_TYPES[fa.cputype], fa_hsh["cputype_sym"] 584 | assert_equal MachO::Headers::CPU_SUBTYPES[fa.cputype][fa.cpusubtype], fa_hsh["cpusubtype_sym"] 585 | end 586 | end 587 | end 588 | -------------------------------------------------------------------------------- /test/test_macho.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers" 4 | 5 | class MachOFileTest < Minitest::Test 6 | include Helpers 7 | 8 | def test_nonexistent_file 9 | assert_raises ArgumentError do 10 | MachO::MachOFile.new("/this/is/a/file/that/cannot/possibly/exist") 11 | end 12 | end 13 | 14 | def test_empty_file 15 | tempfile_with_data("empty_file", "") do |empty_file| 16 | assert_raises MachO::TruncatedFileError do 17 | MachO::MachOFile.new(empty_file.path) 18 | end 19 | end 20 | end 21 | 22 | def test_truncated_file 23 | tempfile_with_data("truncated_file", "\xFE\xED\xFA\xCE\x00\x00") do |truncated_file| 24 | assert_raises MachO::TruncatedFileError do 25 | MachO::MachOFile.new(truncated_file.path) 26 | end 27 | end 28 | end 29 | 30 | def test_load_commands 31 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 32 | 33 | filenames.each do |fn| 34 | file = MachO::MachOFile.new(fn) 35 | 36 | file.load_commands.each do |lc| 37 | assert lc 38 | assert_kind_of MachO::LoadCommands::LoadCommand, lc 39 | assert_kind_of Integer, lc.offset 40 | assert_kind_of Integer, lc.cmd 41 | assert_kind_of Integer, lc.cmdsize 42 | assert_kind_of String, lc.to_s 43 | assert_kind_of Symbol, lc.type 44 | assert_kind_of Symbol, lc.to_sym 45 | end 46 | end 47 | end 48 | 49 | def test_load_commands_well_ordered 50 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "libhello.dylib") } 51 | 52 | filenames.each do |fn| 53 | file = MachO::MachOFile.new(fn) 54 | offsets = file.load_commands.map { |lc| lc.view.offset } 55 | assert_equal offsets.sort, offsets 56 | end 57 | end 58 | 59 | def test_unknown_load_command 60 | filename = fixture(:x86_64, "hello_unk_lc.bin") 61 | 62 | # Unknown load command in non-permissive mode: raise exception. 63 | assert_raises MachO::LoadCommandError do 64 | MachO::MachOFile.new(filename) 65 | end 66 | 67 | # Unknown load command in permissive mode: treat as a generic LoadCommand. 68 | MachO::MachOFile.new(filename, :permissive => true) 69 | end 70 | 71 | def test_mach_header 72 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "libhello.dylib") } 73 | 74 | filenames.each do |fn| 75 | file = MachO::MachOFile.new(fn) 76 | 77 | header = file.header 78 | 79 | assert header 80 | assert_kind_of MachO::Headers::MachHeader, header if file.magic32? 81 | assert_kind_of MachO::Headers::MachHeader64, header if file.magic64? 82 | assert_kind_of Integer, header.magic 83 | assert_kind_of Integer, header.cputype 84 | assert_kind_of Integer, header.cpusubtype 85 | assert_kind_of Integer, header.filetype 86 | assert_kind_of Integer, header.ncmds 87 | assert_kind_of Integer, header.sizeofcmds 88 | assert_kind_of Integer, header.flags 89 | refute header.flag?(:THIS_IS_A_MADE_UP_FLAG) 90 | end 91 | end 92 | 93 | def test_segments_and_sections 94 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hellobundle.so") } 95 | 96 | filenames.each do |fn| 97 | file = MachO::MachOFile.new(fn) 98 | 99 | assert_kind_of Integer, file.segment_alignment 100 | assert_operator file.segment_alignment, :<=, MachO::Sections::MAX_SECT_ALIGN 101 | 102 | segments = file.segments 103 | 104 | assert_kind_of Array, segments 105 | 106 | segments.each do |seg| 107 | assert seg 108 | 109 | assert_kind_of Integer, seg.guess_align 110 | assert_operator seg.guess_align, :<=, MachO::Sections::MAX_SECT_ALIGN 111 | 112 | assert_kind_of MachO::LoadCommands::SegmentCommand, seg if file.magic32? 113 | assert_kind_of MachO::LoadCommands::SegmentCommand64, seg if file.magic64? 114 | assert_kind_of String, seg.segname 115 | assert_equal seg.segname, seg.to_s 116 | assert_kind_of Integer, seg.vmaddr 117 | assert_kind_of Integer, seg.vmsize 118 | assert_kind_of Integer, seg.fileoff 119 | assert_kind_of Integer, seg.filesize 120 | assert_kind_of Integer, seg.maxprot 121 | assert_kind_of Integer, seg.initprot 122 | assert_kind_of Integer, seg.nsects 123 | assert_kind_of Integer, seg.flags 124 | refute seg.flag?(:THIS_IS_A_MADE_UP_FLAG) 125 | assert(MachO::LoadCommands::SEGMENT_FLAGS.keys.one? { |sf| seg.flag?(sf) }) if seg.flags != 0 126 | 127 | sections = seg.sections 128 | 129 | assert_kind_of Array, sections 130 | 131 | sections.each do |sect| 132 | assert sect 133 | 134 | assert_kind_of MachO::Sections::Section, sect if seg.is_a? MachO::LoadCommands::SegmentCommand 135 | assert_kind_of MachO::Sections::Section64, sect if seg.is_a? MachO::LoadCommands::SegmentCommand64 136 | assert_kind_of String, sect.sectname 137 | assert_kind_of String, sect.segname 138 | assert_kind_of Integer, sect.addr 139 | assert_kind_of Integer, sect.size 140 | assert_kind_of Integer, sect.offset 141 | assert_kind_of Integer, sect.align 142 | assert_kind_of Integer, sect.reloff 143 | assert_kind_of Integer, sect.nreloc 144 | assert_kind_of Integer, sect.flags 145 | refute sect.flag?(:THIS_IS_A_MADE_UP_FLAG) 146 | assert_kind_of Integer, sect.type 147 | assert MachO::Sections::SECTION_TYPES.values.include?(sect.type) 148 | assert(MachO::Sections::SECTION_TYPES.keys.one? { |st| sect.type?(st) }) 149 | assert_kind_of Integer, sect.attributes 150 | assert(MachO::Sections::SECTION_ATTRIBUTES.keys.any? { |sa| sect.attribute?(sa) }) 151 | assert_kind_of Integer, sect.reserved1 152 | assert_kind_of Integer, sect.reserved2 153 | assert_kind_of Integer, sect.reserved3 if sect.is_a? MachO::Sections::Section64 154 | end 155 | end 156 | end 157 | end 158 | 159 | def test_file 160 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 161 | 162 | filenames.each do |fn| 163 | file = MachO::MachOFile.new(fn) 164 | 165 | assert file.serialize 166 | assert_kind_of String, file.serialize 167 | 168 | assert_kind_of Integer, file.magic 169 | assert_kind_of String, file.magic_string 170 | assert_kind_of Symbol, file.filetype 171 | assert_kind_of Symbol, file.cputype 172 | assert_kind_of Symbol, file.cpusubtype 173 | assert_kind_of Integer, file.ncmds 174 | assert_kind_of Integer, file.sizeofcmds 175 | assert_kind_of Integer, file.flags 176 | 177 | refute_predicate file.segments, :empty? 178 | refute_predicate file.linked_dylibs, :empty? 179 | end 180 | end 181 | 182 | def test_object 183 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.o") } 184 | 185 | filenames.each do |fn| 186 | file = MachO::MachOFile.new(fn) 187 | 188 | assert file.object? 189 | filechecks(:object?).each do |check| 190 | refute file.send(check) 191 | end 192 | 193 | assert_equal :object, file.filetype 194 | 195 | # it's not a dylib, so it has no dylib id 196 | assert_nil file.dylib_id 197 | end 198 | end 199 | 200 | def test_executable 201 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 202 | 203 | filenames.each do |fn| 204 | file = MachO::MachOFile.new(fn) 205 | 206 | assert file.executable? 207 | filechecks(:executable?).each do |check| 208 | refute file.send(check) 209 | end 210 | 211 | assert_equal :execute, file.filetype 212 | 213 | # it's not a dylib, so it has no dylib id 214 | assert_nil file.dylib_id 215 | end 216 | end 217 | 218 | def test_dylib 219 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "libhello.dylib") } 220 | 221 | filenames.each do |fn| 222 | file = MachO::MachOFile.new(fn) 223 | 224 | assert file.dylib? 225 | filechecks(:dylib?).each do |check| 226 | refute file.send(check) 227 | end 228 | 229 | assert_equal :dylib, file.filetype 230 | 231 | # it's a dylib, so it *must* have a dylib id 232 | assert file.dylib_id 233 | end 234 | end 235 | 236 | def test_extra_dylib 237 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "libextrahello.dylib") } 238 | unusual_dylib_lcs = { 239 | :LC_LOAD_UPWARD_DYLIB => :DYLIB_USE_UPWARD, 240 | :LC_LAZY_LOAD_DYLIB => nil, 241 | :LC_LOAD_WEAK_DYLIB => :DYLIB_USE_WEAK_LINK, 242 | :LC_REEXPORT_DYLIB => :DYLIB_USE_REEXPORT, 243 | } 244 | 245 | filenames.each do |fn| 246 | file = MachO::MachOFile.new(fn) 247 | 248 | assert file.dylib? 249 | 250 | # make sure we can read more unusual dylib load commands 251 | unusual_dylib_lcs.each do |cmdname, flag_name| 252 | lc = file[cmdname].first 253 | 254 | # PPC and x86-family binaries don't have the same dylib LCs, so ignore 255 | # the ones that don't exist 256 | # https://github.com/Homebrew/ruby-macho/pull/24#issuecomment-226287121 257 | next unless lc 258 | 259 | assert_kind_of MachO::LoadCommands::DylibCommand, lc 260 | 261 | dylib_name = lc.name 262 | 263 | assert dylib_name 264 | assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, dylib_name 265 | 266 | assert lc.flag?(flag_name) if flag_name 267 | (unusual_dylib_lcs.values - [flag_name]).compact.each do |other_flag_name| 268 | refute lc.flag?(other_flag_name) 269 | end 270 | end 271 | end 272 | end 273 | 274 | def test_dylib_use_command 275 | filenames = SINGLE_64_ARCHES.map { |a| fixture(a, "dylib_use_command-weak-delay.bin") } 276 | 277 | filenames.each do |fn| 278 | file = MachO::MachOFile.new(fn) 279 | 280 | lc = file[:LC_LOAD_WEAK_DYLIB].first 281 | lc2 = file[:LC_LOAD_DYLIB].first 282 | 283 | assert_instance_of MachO::LoadCommands::DylibUseCommand, lc 284 | assert_instance_of MachO::LoadCommands::DylibCommand, lc2 285 | 286 | refute_equal lc.flags, 0 287 | 288 | assert lc.flag?(:DYLIB_USE_WEAK_LINK) 289 | assert lc.flag?(:DYLIB_USE_DELAYED_INIT) 290 | refute lc.flag?(:DYLIB_USE_UPWARD) 291 | 292 | refute lc2.flag?(:DYLIB_USE_WEAK_LINK) 293 | end 294 | end 295 | 296 | def test_bundle 297 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hellobundle.so") } 298 | 299 | filenames.each do |fn| 300 | file = MachO::MachOFile.new(fn) 301 | 302 | # a file can only be ONE of these 303 | assert file.bundle? 304 | filechecks(:bundle?).each do |check| 305 | refute file.send(check) 306 | end 307 | 308 | assert_equal :bundle, file.filetype 309 | 310 | # it's not a dylib, so it has no dylib id 311 | assert_nil file.dylib_id 312 | end 313 | end 314 | 315 | def test_change_dylib_id 316 | groups = SINGLE_ARCHES.map do |arch| 317 | ["libhello.dylib", "libhello_actual.dylib", "libhello_expected.dylib"].map do |fn| 318 | fixture(arch, fn) 319 | end 320 | end 321 | 322 | groups.each do |filename, actual, expected| 323 | file = MachO::MachOFile.new(filename) 324 | 325 | # changing the dylib id should work 326 | old_id = file.dylib_id 327 | file.dylib_id = "testing" 328 | assert_equal "testing", file.dylib_id 329 | 330 | # change it back within the same instance 331 | file.dylib_id = old_id 332 | assert_equal old_id, file.dylib_id 333 | 334 | refute_predicate file.segments, :empty? 335 | refute_predicate file.linked_dylibs, :empty? 336 | 337 | really_big_id = "x" * 4096 338 | 339 | # test failsafe for excessively large IDs (w/ no special linking) 340 | assert_raises MachO::HeaderPadError do 341 | file.dylib_id = really_big_id 342 | end 343 | 344 | file.dylib_id = "test" 345 | 346 | file.write(actual) 347 | 348 | assert equal_sha1_hashes(actual, expected) 349 | 350 | act = MachO::MachOFile.new(actual) 351 | exp = MachO::MachOFile.new(expected) 352 | 353 | assert_equal file.ncmds, act.ncmds 354 | assert_equal exp.ncmds, act.ncmds 355 | 356 | assert_equal exp.dylib_id, act.dylib_id 357 | end 358 | ensure 359 | groups.each do |_, actual, _| 360 | delete_if_exists(actual) 361 | end 362 | end 363 | 364 | def test_change_install_name 365 | groups = SINGLE_ARCHES.map do |arch| 366 | ["hello.bin", "hello_actual.bin", "hello_expected.bin"].map do |fn| 367 | fixture(arch, fn) 368 | end 369 | end 370 | 371 | groups.each do |filename, actual, expected| 372 | file = MachO::MachOFile.new(filename) 373 | 374 | dylibs = file.linked_dylibs 375 | 376 | # there should be at least one dylib linked to the binary 377 | refute_empty dylibs 378 | 379 | file.change_install_name(dylibs[0], "test") 380 | new_dylibs = file.linked_dylibs 381 | 382 | # the new dylib name should reflect the changes we've made 383 | assert_equal "test", new_dylibs[0] 384 | refute_equal dylibs[0], new_dylibs[0] 385 | 386 | file.write(actual) 387 | 388 | assert equal_sha1_hashes(actual, expected) 389 | 390 | act = MachO::MachOFile.new(actual) 391 | exp = MachO::MachOFile.new(expected) 392 | 393 | assert_equal file.linked_dylibs.size, act.linked_dylibs.size 394 | assert_equal file.ncmds, act.ncmds 395 | assert_equal exp.linked_dylibs.size, act.linked_dylibs.size 396 | assert_equal exp.ncmds, act.ncmds 397 | 398 | assert_equal exp.linked_dylibs.first, act.linked_dylibs.first 399 | end 400 | ensure 401 | groups.each do |_, actual, _| 402 | delete_if_exists(actual) 403 | end 404 | end 405 | 406 | def test_change_install_name_preserves_type 407 | filename = fixture(:i386, "libextrahello.dylib") 408 | 409 | file = MachO::MachOFile.new(filename) 410 | old_dylib_types = file.dylib_load_commands.map(&:type) 411 | # this particular dylib is an LC_LOAD_UPWARD_DYLIB 412 | file.change_install_name("/usr/lib/libz.1.dylib", "test") 413 | new_dylib_types = file.dylib_load_commands.map(&:type) 414 | 415 | assert_equal old_dylib_types, new_dylib_types 416 | end 417 | 418 | def test_get_rpaths 419 | filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") } 420 | 421 | filenames.each do |filename| 422 | file = MachO::MachOFile.new(filename) 423 | rpaths = file.rpaths 424 | 425 | assert_kind_of Array, rpaths 426 | assert_kind_of String, rpaths.first 427 | assert_equal "made_up_path", rpaths.first 428 | end 429 | end 430 | 431 | def test_change_rpath 432 | groups = SINGLE_ARCHES.map do |arch| 433 | ["", "_rpath_actual", "_rpath_expected"].map do |suffix| 434 | fixture(arch, "hello#{suffix}.bin") 435 | end 436 | end 437 | 438 | groups.each do |filename, actual, expected| 439 | file = MachO::MachOFile.new(filename) 440 | 441 | rpaths = file.rpaths 442 | 443 | # there should be at least one rpath in each binary 444 | refute_empty rpaths 445 | 446 | # We should ignore errors when changing to an existing rpath 447 | # This is the same behaviour as `install_name_tool` 448 | file.change_rpath(rpaths.first, rpaths.first) 449 | new_rpaths = file.rpaths 450 | 451 | assert_equal new_rpaths.first, rpaths.first 452 | refute_empty new_rpaths.first, rpaths.first 453 | 454 | file.change_rpath(rpaths.first, "/usr/lib") 455 | new_rpaths = file.rpaths 456 | 457 | # the new rpath should reflect the changes we've made 458 | assert_equal "/usr/lib", new_rpaths.first 459 | refute_empty rpaths.first, new_rpaths.first 460 | 461 | file.write(actual) 462 | 463 | assert equal_sha1_hashes(actual, expected) 464 | 465 | act = MachO::MachOFile.new(actual) 466 | exp = MachO::MachOFile.new(expected) 467 | 468 | assert_equal file.rpaths.size, act.rpaths.size 469 | assert_equal file.ncmds, act.ncmds 470 | assert_equal exp.rpaths.size, act.rpaths.size 471 | assert_equal exp.ncmds, act.ncmds 472 | 473 | assert_equal exp.rpaths.first, act.rpaths.first 474 | end 475 | ensure 476 | groups.each do |_, actual, _| 477 | delete_if_exists(actual) 478 | end 479 | end 480 | 481 | def test_delete_rpath 482 | groups = SINGLE_ARCHES.map do |arch| 483 | ["hello.bin", "hello_actual.bin"].map do |fn| 484 | fixture(arch, fn) 485 | end 486 | end 487 | 488 | groups << ["libdupe.dylib", "libdupe_actual.dylib"].map do |fn| 489 | fixture(:x86_64, fn) 490 | end 491 | 492 | groups.each do |filename, actual| 493 | file = MachO::MachOFile.new(filename) 494 | 495 | refute_empty file.rpaths 496 | orig_ncmds = current_ncmds = file.ncmds 497 | orig_sizeofcmds = file.sizeofcmds 498 | orig_npaths = current_npaths = file.rpaths.size 499 | 500 | file.rpaths.each do |rpath| 501 | file.delete_rpath(rpath) 502 | current_npaths -= 1 503 | current_ncmds -= 1 504 | 505 | assert_equal file.ncmds, current_ncmds 506 | assert_equal file.rpaths.size, current_npaths 507 | assert_operator file.sizeofcmds, :<, orig_sizeofcmds 508 | end 509 | 510 | file.write(actual) 511 | # ensure we can actually re-load and parse the modified file 512 | modified = MachO::MachOFile.new(actual) 513 | 514 | assert_empty modified.rpaths 515 | assert_equal file.serialize.bytesize, modified.serialize.bytesize 516 | assert_operator modified.ncmds, :<, orig_ncmds 517 | assert_operator modified.sizeofcmds, :<, orig_sizeofcmds 518 | assert_equal file.rpaths.size, modified.rpaths.size 519 | assert_operator modified.rpaths.size, :<, orig_npaths 520 | end 521 | ensure 522 | groups.each do |_, actual| 523 | delete_if_exists(actual) 524 | end 525 | end 526 | 527 | def test_delete_rpath_uniq 528 | groups = SINGLE_ARCHES.map do |arch| 529 | ["hello.bin", "hello_actual.bin"].map do |fn| 530 | fixture(arch, fn) 531 | end 532 | end 533 | 534 | groups << ["libdupe.dylib", "libdupe_actual.dylib"].map do |fn| 535 | fixture(:x86_64, fn) 536 | end 537 | 538 | groups.each do |filename, actual| 539 | file = MachO::MachOFile.new(filename) 540 | 541 | refute_empty file.rpaths 542 | orig_ncmds = file.ncmds 543 | orig_sizeofcmds = file.sizeofcmds 544 | orig_npaths = file.rpaths.size 545 | 546 | file.delete_rpath(file.rpaths.first, :uniq => true) 547 | assert_operator file.ncmds, :<, orig_ncmds 548 | assert_operator file.sizeofcmds, :<, orig_sizeofcmds 549 | assert_operator file.rpaths.size, :<, orig_npaths 550 | # libdupe rpaths: ["foo", "bar", "foo"] 551 | assert_equal file.rpaths, ["bar"] if filename.end_with?("libdupe.dylib") 552 | 553 | file.write(actual) 554 | # ensure we can actually re-load and parse the modified file 555 | modified = MachO::MachOFile.new(actual) 556 | 557 | assert_empty modified.rpaths unless filename.end_with?("libdupe.dylib") 558 | assert_equal file.serialize.bytesize, modified.serialize.bytesize 559 | assert_operator modified.ncmds, :<, orig_ncmds 560 | assert_operator modified.sizeofcmds, :<, orig_sizeofcmds 561 | assert_equal file.rpaths.size, modified.rpaths.size 562 | assert_operator modified.rpaths.size, :<, orig_npaths 563 | end 564 | ensure 565 | groups.each do |_, actual| 566 | delete_if_exists(actual) 567 | end 568 | end 569 | 570 | def test_delete_rpath_last 571 | groups = SINGLE_ARCHES.map do |arch| 572 | ["hello.bin", "hello_actual.bin"].map do |fn| 573 | fixture(arch, fn) 574 | end 575 | end 576 | 577 | groups << ["libdupe.dylib", "libdupe_actual.dylib"].map do |fn| 578 | fixture(:x86_64, fn) 579 | end 580 | 581 | groups.each do |filename, actual| 582 | file = MachO::MachOFile.new(filename) 583 | 584 | refute_empty file.rpaths 585 | orig_ncmds = file.ncmds 586 | orig_sizeofcmds = file.sizeofcmds 587 | orig_npaths = file.rpaths.size 588 | 589 | file.delete_rpath(file.rpaths.first, :last => true) 590 | assert_operator file.ncmds, :<, orig_ncmds 591 | assert_operator file.sizeofcmds, :<, orig_sizeofcmds 592 | assert_operator file.rpaths.size, :<, orig_npaths 593 | # libdupe rpaths: ["foo", "bar", "foo"] 594 | assert_equal file.rpaths, %w[foo bar] if filename.end_with?("libdupe.dylib") 595 | 596 | file.write(actual) 597 | # ensure we can actually re-load and parse the modified file 598 | modified = MachO::MachOFile.new(actual) 599 | 600 | assert_empty modified.rpaths unless filename.end_with?("libdupe.dylib") 601 | assert_equal file.serialize.bytesize, modified.serialize.bytesize 602 | assert_operator modified.ncmds, :<, orig_ncmds 603 | assert_operator modified.sizeofcmds, :<, orig_sizeofcmds 604 | assert_equal file.rpaths.size, modified.rpaths.size 605 | assert_operator modified.rpaths.size, :<, orig_npaths 606 | end 607 | ensure 608 | groups.each do |_, actual| 609 | delete_if_exists(actual) 610 | end 611 | end 612 | 613 | def test_add_rpath 614 | groups = SINGLE_ARCHES.map do |arch| 615 | ["hello.bin", "hello_actual.bin"].map do |fn| 616 | fixture(arch, fn) 617 | end 618 | end 619 | 620 | groups.each do |filename, actual| 621 | file = MachO::MachOFile.new(filename) 622 | 623 | orig_ncmds = file.ncmds 624 | orig_sizeofcmds = file.sizeofcmds 625 | orig_npaths = file.rpaths.size 626 | 627 | file.add_rpath("/foo/bar/baz") 628 | assert_operator file.ncmds, :>, orig_ncmds 629 | assert_operator file.sizeofcmds, :>, orig_sizeofcmds 630 | assert_operator file.rpaths.size, :>, orig_npaths 631 | assert_includes file.rpaths, "/foo/bar/baz" 632 | 633 | file.write(actual) 634 | # ensure we can actually re-load and parse the modified file 635 | modified = MachO::MachOFile.new(actual) 636 | 637 | assert_equal file.serialize.bytesize, modified.serialize.bytesize 638 | assert_operator modified.ncmds, :>, orig_ncmds 639 | assert_operator modified.sizeofcmds, :>, orig_sizeofcmds 640 | assert_equal file.rpaths.size, modified.rpaths.size 641 | assert_operator modified.rpaths.size, :>, orig_npaths 642 | assert_includes modified.rpaths, "/foo/bar/baz" 643 | end 644 | ensure 645 | groups.each do |_, actual| 646 | delete_if_exists(actual) 647 | end 648 | end 649 | 650 | def test_rpath_exceptions 651 | filename = fixture(:i386, "hello.bin") 652 | file = MachO::MachOFile.new(filename) 653 | 654 | assert_raises MachO::RpathUnknownError do 655 | file.change_rpath("/this/rpath/doesn't/exist", "/lib") 656 | end 657 | 658 | assert_raises MachO::RpathExistsError do 659 | file.add_rpath(file.rpaths.first) 660 | end 661 | 662 | assert_raises MachO::RpathUnknownError do 663 | file.delete_rpath("/this/rpath/doesn't/exist") 664 | end 665 | end 666 | 667 | def test_fail_loading_fat 668 | filename = fixture(%w[i386 x86_64], "libhello.dylib") 669 | 670 | ex = assert_raises(MachO::FatBinaryError) do 671 | MachO::MachOFile.new_from_bin File.read(filename) 672 | end 673 | 674 | assert_match(/must be/, ex.inspect) 675 | end 676 | 677 | def test_to_h 678 | filename = fixture(:i386, "hello.bin") 679 | file = MachO::MachOFile.new(filename) 680 | hsh = file.to_h 681 | 682 | header_fields = %w[ 683 | magic 684 | cputype 685 | cpusubtype 686 | filetype 687 | ncmds 688 | sizeofcmds 689 | flags 690 | alignment 691 | ] 692 | 693 | # fields in the header should be the same as in the hash representation 694 | header_fields.each do |field| 695 | assert_equal file.header.send(field), hsh["header"][field] 696 | end 697 | 698 | # additionally, symbol keys in the hash representation should correspond 699 | # to looked-up values in the header 700 | assert_equal MachO::Headers::MH_MAGICS[file.header.magic], hsh["header"]["magic_sym"] 701 | assert_equal MachO::Headers::CPU_TYPES[file.header.cputype], hsh["header"]["cputype_sym"] 702 | assert_equal MachO::Headers::CPU_SUBTYPES[file.header.cputype][file.header.cpusubtype], hsh["header"]["cpusubtype_sym"] 703 | assert_equal MachO::Headers::MH_FILETYPES[file.header.filetype], hsh["header"]["filetype_sym"] 704 | 705 | # the number of load commands should be the same in the hash representation 706 | assert_equal file.load_commands.size, hsh["load_commands"].size 707 | 708 | hsh["load_commands"].each do |lc_hsh| 709 | # each load command should have, at minimum, a cmd, cmdsize, type, view, and structure 710 | assert_kind_of Integer, lc_hsh["cmd"] 711 | assert_kind_of Integer, lc_hsh["cmdsize"] 712 | assert_kind_of Symbol, lc_hsh["type"] 713 | assert_kind_of Hash, lc_hsh["view"] 714 | assert_kind_of Hash, lc_hsh["structure"] 715 | 716 | # when looked up, cmd should correspond to type 717 | assert_equal lc_hsh["type"], MachO::LoadCommands::LOAD_COMMANDS[lc_hsh["cmd"]] 718 | 719 | # the view should contain an endianness and an offset 720 | assert_kind_of Symbol, lc_hsh["view"]["endianness"] 721 | assert_kind_of Integer, lc_hsh["view"]["offset"] 722 | 723 | # the structure should contain a format and a bytesize 724 | assert_kind_of String, lc_hsh["structure"]["format"] 725 | assert_kind_of Integer, lc_hsh["structure"]["bytesize"] 726 | end 727 | end 728 | end 729 | -------------------------------------------------------------------------------- /lib/macho/headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MachO 4 | # Classes and constants for parsing the headers of Mach-O binaries. 5 | module Headers 6 | # big-endian fat magic 7 | # @api private 8 | FAT_MAGIC = 0xcafebabe 9 | 10 | # little-endian fat magic 11 | # @note This is defined for completeness, but should never appear in ruby-macho code, 12 | # since fat headers are always big-endian. 13 | # @api private 14 | FAT_CIGAM = 0xbebafeca 15 | 16 | # 64-bit big-endian fat magic 17 | FAT_MAGIC_64 = 0xcafebabf 18 | 19 | # 64-bit little-endian fat magic 20 | # @note This is defined for completeness, but should never appear in ruby-macho code, 21 | # since fat headers are always big-endian. 22 | FAT_CIGAM_64 = 0xbfbafeca 23 | 24 | # 32-bit big-endian magic 25 | # @api private 26 | MH_MAGIC = 0xfeedface 27 | 28 | # 32-bit little-endian magic 29 | # @api private 30 | MH_CIGAM = 0xcefaedfe 31 | 32 | # 64-bit big-endian magic 33 | # @api private 34 | MH_MAGIC_64 = 0xfeedfacf 35 | 36 | # 64-bit little-endian magic 37 | # @api private 38 | MH_CIGAM_64 = 0xcffaedfe 39 | 40 | # compressed mach-o magic 41 | # @api private 42 | COMPRESSED_MAGIC = 0x636f6d70 # "comp" 43 | 44 | # a compressed mach-o slice, using LZSS for compression 45 | # @api private 46 | COMP_TYPE_LZSS = 0x6c7a7373 # "lzss" 47 | 48 | # a compressed mach-o slice, using LZVN ("FastLib") for compression 49 | # @api private 50 | COMP_TYPE_FASTLIB = 0x6c7a766e # "lzvn" 51 | 52 | # association of magic numbers to string representations 53 | # @api private 54 | MH_MAGICS = { 55 | FAT_MAGIC => "FAT_MAGIC", 56 | FAT_MAGIC_64 => "FAT_MAGIC_64", 57 | MH_MAGIC => "MH_MAGIC", 58 | MH_CIGAM => "MH_CIGAM", 59 | MH_MAGIC_64 => "MH_MAGIC_64", 60 | MH_CIGAM_64 => "MH_CIGAM_64", 61 | }.freeze 62 | 63 | # mask for CPUs with 64-bit architectures (when running a 64-bit ABI?) 64 | # @api private 65 | CPU_ARCH_ABI64 = 0x01000000 66 | 67 | # mask for CPUs with 64-bit architectures (when running a 32-bit ABI?) 68 | # @see https://github.com/Homebrew/ruby-macho/issues/113 69 | # @api private 70 | CPU_ARCH_ABI32 = 0x02000000 71 | 72 | # any CPU (unused?) 73 | # @api private 74 | CPU_TYPE_ANY = -1 75 | 76 | # m68k compatible CPUs 77 | # @api private 78 | CPU_TYPE_MC680X0 = 0x06 79 | 80 | # i386 and later compatible CPUs 81 | # @api private 82 | CPU_TYPE_I386 = 0x07 83 | 84 | # x86_64 (AMD64) compatible CPUs 85 | # @api private 86 | CPU_TYPE_X86_64 = (CPU_TYPE_I386 | CPU_ARCH_ABI64) 87 | 88 | # 32-bit ARM compatible CPUs 89 | # @api private 90 | CPU_TYPE_ARM = 0x0c 91 | 92 | # m88k compatible CPUs 93 | # @api private 94 | CPU_TYPE_MC88000 = 0xd 95 | 96 | # 64-bit ARM compatible CPUs 97 | # @api private 98 | CPU_TYPE_ARM64 = (CPU_TYPE_ARM | CPU_ARCH_ABI64) 99 | 100 | # 64-bit ARM compatible CPUs (running in 32-bit mode?) 101 | # @see https://github.com/Homebrew/ruby-macho/issues/113 102 | CPU_TYPE_ARM64_32 = (CPU_TYPE_ARM | CPU_ARCH_ABI32) 103 | 104 | # PowerPC compatible CPUs 105 | # @api private 106 | CPU_TYPE_POWERPC = 0x12 107 | 108 | # PowerPC64 compatible CPUs 109 | # @api private 110 | CPU_TYPE_POWERPC64 = (CPU_TYPE_POWERPC | CPU_ARCH_ABI64) 111 | 112 | # association of cpu types to symbol representations 113 | # @api private 114 | CPU_TYPES = { 115 | CPU_TYPE_ANY => :any, 116 | CPU_TYPE_I386 => :i386, 117 | CPU_TYPE_X86_64 => :x86_64, 118 | CPU_TYPE_ARM => :arm, 119 | CPU_TYPE_ARM64 => :arm64, 120 | CPU_TYPE_ARM64_32 => :arm64_32, 121 | CPU_TYPE_POWERPC => :ppc, 122 | CPU_TYPE_POWERPC64 => :ppc64, 123 | }.freeze 124 | 125 | # mask for CPU subtype capabilities 126 | # @api private 127 | CPU_SUBTYPE_MASK = 0xff000000 128 | 129 | # 64-bit libraries (undocumented!) 130 | # @see http://llvm.org/docs/doxygen/html/Support_2MachO_8h_source.html 131 | # @api private 132 | CPU_SUBTYPE_LIB64 = 0x80000000 133 | 134 | # the lowest common sub-type for `CPU_TYPE_I386` 135 | # @api private 136 | CPU_SUBTYPE_I386 = 3 137 | 138 | # the i486 sub-type for `CPU_TYPE_I386` 139 | # @api private 140 | CPU_SUBTYPE_486 = 4 141 | 142 | # the i486SX sub-type for `CPU_TYPE_I386` 143 | # @api private 144 | CPU_SUBTYPE_486SX = 132 145 | 146 | # the i586 (P5, Pentium) sub-type for `CPU_TYPE_I386` 147 | # @api private 148 | CPU_SUBTYPE_586 = 5 149 | 150 | # @see CPU_SUBTYPE_586 151 | # @api private 152 | CPU_SUBTYPE_PENT = CPU_SUBTYPE_586 153 | 154 | # the Pentium Pro (P6) sub-type for `CPU_TYPE_I386` 155 | # @api private 156 | CPU_SUBTYPE_PENTPRO = 22 157 | 158 | # the Pentium II (P6, M3?) sub-type for `CPU_TYPE_I386` 159 | # @api private 160 | CPU_SUBTYPE_PENTII_M3 = 54 161 | 162 | # the Pentium II (P6, M5?) sub-type for `CPU_TYPE_I386` 163 | # @api private 164 | CPU_SUBTYPE_PENTII_M5 = 86 165 | 166 | # the Pentium 4 (Netburst) sub-type for `CPU_TYPE_I386` 167 | # @api private 168 | CPU_SUBTYPE_PENTIUM_4 = 10 169 | 170 | # the lowest common sub-type for `CPU_TYPE_MC680X0` 171 | # @api private 172 | CPU_SUBTYPE_MC680X0_ALL = 1 173 | 174 | # @see CPU_SUBTYPE_MC680X0_ALL 175 | # @api private 176 | CPU_SUBTYPE_MC68030 = CPU_SUBTYPE_MC680X0_ALL 177 | 178 | # the 040 subtype for `CPU_TYPE_MC680X0` 179 | # @api private 180 | CPU_SUBTYPE_MC68040 = 2 181 | 182 | # the 030 subtype for `CPU_TYPE_MC680X0` 183 | # @api private 184 | CPU_SUBTYPE_MC68030_ONLY = 3 185 | 186 | # the lowest common sub-type for `CPU_TYPE_X86_64` 187 | # @api private 188 | CPU_SUBTYPE_X86_64_ALL = CPU_SUBTYPE_I386 189 | 190 | # the Haskell sub-type for `CPU_TYPE_X86_64` 191 | # @api private 192 | CPU_SUBTYPE_X86_64_H = 8 193 | 194 | # the lowest common sub-type for `CPU_TYPE_ARM` 195 | # @api private 196 | CPU_SUBTYPE_ARM_ALL = 0 197 | 198 | # the v4t sub-type for `CPU_TYPE_ARM` 199 | # @api private 200 | CPU_SUBTYPE_ARM_V4T = 5 201 | 202 | # the v6 sub-type for `CPU_TYPE_ARM` 203 | # @api private 204 | CPU_SUBTYPE_ARM_V6 = 6 205 | 206 | # the v5 sub-type for `CPU_TYPE_ARM` 207 | # @api private 208 | CPU_SUBTYPE_ARM_V5TEJ = 7 209 | 210 | # the xscale (v5 family) sub-type for `CPU_TYPE_ARM` 211 | # @api private 212 | CPU_SUBTYPE_ARM_XSCALE = 8 213 | 214 | # the v7 sub-type for `CPU_TYPE_ARM` 215 | # @api private 216 | CPU_SUBTYPE_ARM_V7 = 9 217 | 218 | # the v7f (Cortex A9) sub-type for `CPU_TYPE_ARM` 219 | # @api private 220 | CPU_SUBTYPE_ARM_V7F = 10 221 | 222 | # the v7s ("Swift") sub-type for `CPU_TYPE_ARM` 223 | # @api private 224 | CPU_SUBTYPE_ARM_V7S = 11 225 | 226 | # the v7k ("Kirkwood40") sub-type for `CPU_TYPE_ARM` 227 | # @api private 228 | CPU_SUBTYPE_ARM_V7K = 12 229 | 230 | # the v6m sub-type for `CPU_TYPE_ARM` 231 | # @api private 232 | CPU_SUBTYPE_ARM_V6M = 14 233 | 234 | # the v7m sub-type for `CPU_TYPE_ARM` 235 | # @api private 236 | CPU_SUBTYPE_ARM_V7M = 15 237 | 238 | # the v7em sub-type for `CPU_TYPE_ARM` 239 | # @api private 240 | CPU_SUBTYPE_ARM_V7EM = 16 241 | 242 | # the v8 sub-type for `CPU_TYPE_ARM` 243 | # @api private 244 | CPU_SUBTYPE_ARM_V8 = 13 245 | 246 | # the lowest common sub-type for `CPU_TYPE_ARM64` 247 | # @api private 248 | CPU_SUBTYPE_ARM64_ALL = 0 249 | 250 | # the v8 sub-type for `CPU_TYPE_ARM64` 251 | # @api private 252 | CPU_SUBTYPE_ARM64_V8 = 1 253 | 254 | # the v8 sub-type for `CPU_TYPE_ARM64_32` 255 | # @api private 256 | CPU_SUBTYPE_ARM64_32_V8 = 1 257 | 258 | # the e (A12) sub-type for `CPU_TYPE_ARM64` 259 | # @api private 260 | CPU_SUBTYPE_ARM64E = 2 261 | 262 | # the lowest common sub-type for `CPU_TYPE_MC88000` 263 | # @api private 264 | CPU_SUBTYPE_MC88000_ALL = 0 265 | 266 | # @see CPU_SUBTYPE_MC88000_ALL 267 | # @api private 268 | CPU_SUBTYPE_MMAX_JPC = CPU_SUBTYPE_MC88000_ALL 269 | 270 | # the 100 sub-type for `CPU_TYPE_MC88000` 271 | # @api private 272 | CPU_SUBTYPE_MC88100 = 1 273 | 274 | # the 110 sub-type for `CPU_TYPE_MC88000` 275 | # @api private 276 | CPU_SUBTYPE_MC88110 = 2 277 | 278 | # the lowest common sub-type for `CPU_TYPE_POWERPC` 279 | # @api private 280 | CPU_SUBTYPE_POWERPC_ALL = 0 281 | 282 | # the 601 sub-type for `CPU_TYPE_POWERPC` 283 | # @api private 284 | CPU_SUBTYPE_POWERPC_601 = 1 285 | 286 | # the 602 sub-type for `CPU_TYPE_POWERPC` 287 | # @api private 288 | CPU_SUBTYPE_POWERPC_602 = 2 289 | 290 | # the 603 sub-type for `CPU_TYPE_POWERPC` 291 | # @api private 292 | CPU_SUBTYPE_POWERPC_603 = 3 293 | 294 | # the 603e (G2) sub-type for `CPU_TYPE_POWERPC` 295 | # @api private 296 | CPU_SUBTYPE_POWERPC_603E = 4 297 | 298 | # the 603ev sub-type for `CPU_TYPE_POWERPC` 299 | # @api private 300 | CPU_SUBTYPE_POWERPC_603EV = 5 301 | 302 | # the 604 sub-type for `CPU_TYPE_POWERPC` 303 | # @api private 304 | CPU_SUBTYPE_POWERPC_604 = 6 305 | 306 | # the 604e sub-type for `CPU_TYPE_POWERPC` 307 | # @api private 308 | CPU_SUBTYPE_POWERPC_604E = 7 309 | 310 | # the 620 sub-type for `CPU_TYPE_POWERPC` 311 | # @api private 312 | CPU_SUBTYPE_POWERPC_620 = 8 313 | 314 | # the 750 (G3) sub-type for `CPU_TYPE_POWERPC` 315 | # @api private 316 | CPU_SUBTYPE_POWERPC_750 = 9 317 | 318 | # the 7400 (G4) sub-type for `CPU_TYPE_POWERPC` 319 | # @api private 320 | CPU_SUBTYPE_POWERPC_7400 = 10 321 | 322 | # the 7450 (G4 "Voyager") sub-type for `CPU_TYPE_POWERPC` 323 | # @api private 324 | CPU_SUBTYPE_POWERPC_7450 = 11 325 | 326 | # the 970 (G5) sub-type for `CPU_TYPE_POWERPC` 327 | # @api private 328 | CPU_SUBTYPE_POWERPC_970 = 100 329 | 330 | # any CPU sub-type for CPU type `CPU_TYPE_POWERPC64` 331 | # @api private 332 | CPU_SUBTYPE_POWERPC64_ALL = CPU_SUBTYPE_POWERPC_ALL 333 | 334 | # association of CPU types/subtype pairs to symbol representations in 335 | # (very) roughly descending order of commonness 336 | # @see https://opensource.apple.com/source/cctools/cctools-877.8/libstuff/arch.c 337 | # @api private 338 | CPU_SUBTYPES = { 339 | CPU_TYPE_I386 => { 340 | CPU_SUBTYPE_I386 => :i386, 341 | CPU_SUBTYPE_486 => :i486, 342 | CPU_SUBTYPE_486SX => :i486SX, 343 | CPU_SUBTYPE_586 => :i586, # also "pentium" in arch(3) 344 | CPU_SUBTYPE_PENTPRO => :i686, # also "pentpro" in arch(3) 345 | CPU_SUBTYPE_PENTII_M3 => :pentIIm3, 346 | CPU_SUBTYPE_PENTII_M5 => :pentIIm5, 347 | CPU_SUBTYPE_PENTIUM_4 => :pentium4, 348 | }.freeze, 349 | CPU_TYPE_X86_64 => { 350 | CPU_SUBTYPE_X86_64_ALL => :x86_64, 351 | CPU_SUBTYPE_X86_64_H => :x86_64h, 352 | }.freeze, 353 | CPU_TYPE_ARM => { 354 | CPU_SUBTYPE_ARM_ALL => :arm, 355 | CPU_SUBTYPE_ARM_V4T => :armv4t, 356 | CPU_SUBTYPE_ARM_V6 => :armv6, 357 | CPU_SUBTYPE_ARM_V5TEJ => :armv5, 358 | CPU_SUBTYPE_ARM_XSCALE => :xscale, 359 | CPU_SUBTYPE_ARM_V7 => :armv7, 360 | CPU_SUBTYPE_ARM_V7F => :armv7f, 361 | CPU_SUBTYPE_ARM_V7S => :armv7s, 362 | CPU_SUBTYPE_ARM_V7K => :armv7k, 363 | CPU_SUBTYPE_ARM_V6M => :armv6m, 364 | CPU_SUBTYPE_ARM_V7M => :armv7m, 365 | CPU_SUBTYPE_ARM_V7EM => :armv7em, 366 | CPU_SUBTYPE_ARM_V8 => :armv8, 367 | }.freeze, 368 | CPU_TYPE_ARM64 => { 369 | CPU_SUBTYPE_ARM64_ALL => :arm64, 370 | CPU_SUBTYPE_ARM64_V8 => :arm64v8, 371 | CPU_SUBTYPE_ARM64E => :arm64e, 372 | }.freeze, 373 | CPU_TYPE_ARM64_32 => { 374 | CPU_SUBTYPE_ARM64_32_V8 => :arm64_32v8, 375 | }.freeze, 376 | CPU_TYPE_POWERPC => { 377 | CPU_SUBTYPE_POWERPC_ALL => :ppc, 378 | CPU_SUBTYPE_POWERPC_601 => :ppc601, 379 | CPU_SUBTYPE_POWERPC_603 => :ppc603, 380 | CPU_SUBTYPE_POWERPC_603E => :ppc603e, 381 | CPU_SUBTYPE_POWERPC_603EV => :ppc603ev, 382 | CPU_SUBTYPE_POWERPC_604 => :ppc604, 383 | CPU_SUBTYPE_POWERPC_604E => :ppc604e, 384 | CPU_SUBTYPE_POWERPC_750 => :ppc750, 385 | CPU_SUBTYPE_POWERPC_7400 => :ppc7400, 386 | CPU_SUBTYPE_POWERPC_7450 => :ppc7450, 387 | CPU_SUBTYPE_POWERPC_970 => :ppc970, 388 | }.freeze, 389 | CPU_TYPE_POWERPC64 => { 390 | CPU_SUBTYPE_POWERPC64_ALL => :ppc64, 391 | # apparently the only exception to the naming scheme 392 | CPU_SUBTYPE_POWERPC_970 => :ppc970_64, 393 | }.freeze, 394 | CPU_TYPE_MC680X0 => { 395 | CPU_SUBTYPE_MC680X0_ALL => :m68k, 396 | CPU_SUBTYPE_MC68030 => :mc68030, 397 | CPU_SUBTYPE_MC68040 => :mc68040, 398 | }, 399 | CPU_TYPE_MC88000 => { 400 | CPU_SUBTYPE_MC88000_ALL => :m88k, 401 | }, 402 | }.freeze 403 | 404 | # relocatable object file 405 | # @api private 406 | MH_OBJECT = 0x1 407 | 408 | # demand paged executable file 409 | # @api private 410 | MH_EXECUTE = 0x2 411 | 412 | # fixed VM shared library file 413 | # @api private 414 | MH_FVMLIB = 0x3 415 | 416 | # core dump file 417 | # @api private 418 | MH_CORE = 0x4 419 | 420 | # preloaded executable file 421 | # @api private 422 | MH_PRELOAD = 0x5 423 | 424 | # dynamically bound shared library 425 | # @api private 426 | MH_DYLIB = 0x6 427 | 428 | # dynamic link editor 429 | # @api private 430 | MH_DYLINKER = 0x7 431 | 432 | # dynamically bound bundle file 433 | # @api private 434 | MH_BUNDLE = 0x8 435 | 436 | # shared library stub for static linking only, no section contents 437 | # @api private 438 | MH_DYLIB_STUB = 0x9 439 | 440 | # companion file with only debug sections 441 | # @api private 442 | MH_DSYM = 0xa 443 | 444 | # x86_64 kexts 445 | # @api private 446 | MH_KEXT_BUNDLE = 0xb 447 | 448 | # a set of Mach-Os, running in the same userspace, sharing a linkedit. The kext collection files are an example 449 | # of this object type 450 | # @api private 451 | MH_FILESET = 0xc 452 | 453 | # gpu program 454 | # @api private 455 | MH_GPU_EXECUTE = 0xd 456 | 457 | # gpu support functions 458 | # @api private 459 | MH_GPU_DYLIB = 0xe 460 | 461 | # association of filetypes to Symbol representations 462 | # @api private 463 | MH_FILETYPES = { 464 | MH_OBJECT => :object, 465 | MH_EXECUTE => :execute, 466 | MH_FVMLIB => :fvmlib, 467 | MH_CORE => :core, 468 | MH_PRELOAD => :preload, 469 | MH_DYLIB => :dylib, 470 | MH_DYLINKER => :dylinker, 471 | MH_BUNDLE => :bundle, 472 | MH_DYLIB_STUB => :dylib_stub, 473 | MH_DSYM => :dsym, 474 | MH_KEXT_BUNDLE => :kext_bundle, 475 | MH_FILESET => :fileset, 476 | MH_GPU_EXECUTE => :gpu_execute, 477 | MH_GPU_DYLIB => :gpu_dylib, 478 | }.freeze 479 | 480 | # association of mach header flag symbols to values 481 | # @api private 482 | MH_FLAGS = { 483 | :MH_NOUNDEFS => 0x1, 484 | :MH_INCRLINK => 0x2, 485 | :MH_DYLDLINK => 0x4, 486 | :MH_BINDATLOAD => 0x8, 487 | :MH_PREBOUND => 0x10, 488 | :MH_SPLIT_SEGS => 0x20, 489 | :MH_LAZY_INIT => 0x40, 490 | :MH_TWOLEVEL => 0x80, 491 | :MH_FORCE_FLAT => 0x100, 492 | :MH_NOMULTIDEFS => 0x200, 493 | :MH_NOPREFIXBINDING => 0x400, 494 | :MH_PREBINDABLE => 0x800, 495 | :MH_ALLMODSBOUND => 0x1000, 496 | :MH_SUBSECTIONS_VIA_SYMBOLS => 0x2000, 497 | :MH_CANONICAL => 0x4000, 498 | :MH_WEAK_DEFINES => 0x8000, 499 | :MH_BINDS_TO_WEAK => 0x10000, 500 | :MH_ALLOW_STACK_EXECUTION => 0x20000, 501 | :MH_ROOT_SAFE => 0x40000, 502 | :MH_SETUID_SAFE => 0x80000, 503 | :MH_NO_REEXPORTED_DYLIBS => 0x100000, 504 | :MH_PIE => 0x200000, 505 | :MH_DEAD_STRIPPABLE_DYLIB => 0x400000, 506 | :MH_HAS_TLV_DESCRIPTORS => 0x800000, 507 | :MH_NO_HEAP_EXECUTION => 0x1000000, 508 | :MH_APP_EXTENSION_SAFE => 0x2000000, 509 | :MH_NLIST_OUTOFSYNC_WITH_DYLDINFO => 0x4000000, 510 | :MH_SIM_SUPPORT => 0x8000000, 511 | :MH_DYLIB_IN_CACHE => 0x80000000, 512 | }.freeze 513 | 514 | # Fat binary header structure 515 | # @see MachO::FatArch 516 | class FatHeader < MachOStructure 517 | # @return [Integer] the magic number of the header (and file) 518 | field :magic, :uint32, :endian => :big 519 | 520 | # @return [Integer] the number of fat architecture structures following the header 521 | field :nfat_arch, :uint32, :endian => :big 522 | 523 | # @return [String] the serialized fields of the fat header 524 | def serialize 525 | [magic, nfat_arch].pack(self.class.format) 526 | end 527 | 528 | # @return [Hash] a hash representation of this {FatHeader} 529 | def to_h 530 | { 531 | "magic" => magic, 532 | "magic_sym" => MH_MAGICS[magic], 533 | "nfat_arch" => nfat_arch, 534 | }.merge super 535 | end 536 | end 537 | 538 | # 32-bit fat binary header architecture structure. A 32-bit fat Mach-O has one or more of 539 | # these, indicating one or more internal Mach-O blobs. 540 | # @note "32-bit" indicates the fact that this structure stores 32-bit offsets, not that the 541 | # Mach-Os that it points to necessarily *are* 32-bit. 542 | # @see MachO::Headers::FatHeader 543 | class FatArch < MachOStructure 544 | # @return [Integer] the CPU type of the Mach-O 545 | field :cputype, :uint32, :endian => :big 546 | 547 | # @return [Integer] the CPU subtype of the Mach-O 548 | field :cpusubtype, :uint32, :endian => :big, :mask => CPU_SUBTYPE_MASK 549 | 550 | # @return [Integer] the file offset to the beginning of the Mach-O data 551 | field :offset, :uint32, :endian => :big 552 | 553 | # @return [Integer] the size, in bytes, of the Mach-O data 554 | field :size, :uint32, :endian => :big 555 | 556 | # @return [Integer] the alignment, as a power of 2 557 | field :align, :uint32, :endian => :big 558 | 559 | # @return [String] the serialized fields of the fat arch 560 | def serialize 561 | [cputype, cpusubtype, offset, size, align].pack(self.class.format) 562 | end 563 | 564 | # @return [Hash] a hash representation of this {FatArch} 565 | def to_h 566 | { 567 | "cputype" => cputype, 568 | "cputype_sym" => CPU_TYPES[cputype], 569 | "cpusubtype" => cpusubtype, 570 | "cpusubtype_sym" => CPU_SUBTYPES[cputype][cpusubtype], 571 | "offset" => offset, 572 | "size" => size, 573 | "align" => align, 574 | }.merge super 575 | end 576 | end 577 | 578 | # 64-bit fat binary header architecture structure. A 64-bit fat Mach-O has one or more of 579 | # these, indicating one or more internal Mach-O blobs. 580 | # @note "64-bit" indicates the fact that this structure stores 64-bit offsets, not that the 581 | # Mach-Os that it points to necessarily *are* 64-bit. 582 | # @see MachO::Headers::FatHeader 583 | class FatArch64 < FatArch 584 | # @return [Integer] the file offset to the beginning of the Mach-O data 585 | field :offset, :uint64, :endian => :big 586 | 587 | # @return [Integer] the size, in bytes, of the Mach-O data 588 | field :size, :uint64, :endian => :big 589 | 590 | # @return [void] 591 | field :reserved, :uint32, :endian => :big, :default => 0 592 | 593 | # @return [String] the serialized fields of the fat arch 594 | def serialize 595 | [cputype, cpusubtype, offset, size, align, reserved].pack(self.class.format) 596 | end 597 | 598 | # @return [Hash] a hash representation of this {FatArch64} 599 | def to_h 600 | { 601 | "reserved" => reserved, 602 | }.merge super 603 | end 604 | end 605 | 606 | # 32-bit Mach-O file header structure 607 | class MachHeader < MachOStructure 608 | # @return [Integer] the magic number 609 | field :magic, :uint32 610 | 611 | # @return [Integer] the CPU type of the Mach-O 612 | field :cputype, :uint32 613 | 614 | # @return [Integer] the CPU subtype of the Mach-O 615 | field :cpusubtype, :uint32, :mask => CPU_SUBTYPE_MASK 616 | 617 | # @return [Integer] the file type of the Mach-O 618 | field :filetype, :uint32 619 | 620 | # @return [Integer] the number of load commands in the Mach-O 621 | field :ncmds, :uint32 622 | 623 | # @return [Integer] the size of all load commands, in bytes, in the Mach-O 624 | field :sizeofcmds, :uint32 625 | 626 | # @return [Integer] the header flags associated with the Mach-O 627 | field :flags, :uint32 628 | 629 | # @example 630 | # puts "this mach-o has position-independent execution" if header.flag?(:MH_PIE) 631 | # @param flag [Symbol] a mach header flag symbol 632 | # @return [Boolean] true if `flag` is present in the header's flag section 633 | def flag?(flag) 634 | flag = MH_FLAGS[flag] 635 | 636 | return false if flag.nil? 637 | 638 | flags & flag == flag 639 | end 640 | 641 | # @return [Boolean] whether or not the file is of type `MH_OBJECT` 642 | def object? 643 | filetype == Headers::MH_OBJECT 644 | end 645 | 646 | # @return [Boolean] whether or not the file is of type `MH_EXECUTE` 647 | def executable? 648 | filetype == Headers::MH_EXECUTE 649 | end 650 | 651 | # @return [Boolean] whether or not the file is of type `MH_FVMLIB` 652 | def fvmlib? 653 | filetype == Headers::MH_FVMLIB 654 | end 655 | 656 | # @return [Boolean] whether or not the file is of type `MH_CORE` 657 | def core? 658 | filetype == Headers::MH_CORE 659 | end 660 | 661 | # @return [Boolean] whether or not the file is of type `MH_PRELOAD` 662 | def preload? 663 | filetype == Headers::MH_PRELOAD 664 | end 665 | 666 | # @return [Boolean] whether or not the file is of type `MH_DYLIB` 667 | def dylib? 668 | filetype == Headers::MH_DYLIB 669 | end 670 | 671 | # @return [Boolean] whether or not the file is of type `MH_DYLINKER` 672 | def dylinker? 673 | filetype == Headers::MH_DYLINKER 674 | end 675 | 676 | # @return [Boolean] whether or not the file is of type `MH_BUNDLE` 677 | def bundle? 678 | filetype == Headers::MH_BUNDLE 679 | end 680 | 681 | # @return [Boolean] whether or not the file is of type `MH_DSYM` 682 | def dsym? 683 | filetype == Headers::MH_DSYM 684 | end 685 | 686 | # @return [Boolean] whether or not the file is of type `MH_KEXT_BUNDLE` 687 | def kext? 688 | filetype == Headers::MH_KEXT_BUNDLE 689 | end 690 | 691 | # @return [Boolean] whether or not the file is of type `MH_FILESET` 692 | def fileset? 693 | filetype == Headers::MH_FILESET 694 | end 695 | 696 | # @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise 697 | def magic32? 698 | Utils.magic32?(magic) 699 | end 700 | 701 | # @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise 702 | def magic64? 703 | Utils.magic64?(magic) 704 | end 705 | 706 | # @return [Integer] the file's internal alignment 707 | def alignment 708 | magic32? ? 4 : 8 709 | end 710 | 711 | # @return [Hash] a hash representation of this {MachHeader} 712 | def to_h 713 | { 714 | "magic" => magic, 715 | "magic_sym" => MH_MAGICS[magic], 716 | "cputype" => cputype, 717 | "cputype_sym" => CPU_TYPES[cputype], 718 | "cpusubtype" => cpusubtype, 719 | "cpusubtype_sym" => CPU_SUBTYPES[cputype][cpusubtype], 720 | "filetype" => filetype, 721 | "filetype_sym" => MH_FILETYPES[filetype], 722 | "ncmds" => ncmds, 723 | "sizeofcmds" => sizeofcmds, 724 | "flags" => flags, 725 | "alignment" => alignment, 726 | }.merge super 727 | end 728 | end 729 | 730 | # 64-bit Mach-O file header structure 731 | class MachHeader64 < MachHeader 732 | # @return [void] 733 | field :reserved, :uint32 734 | 735 | # @return [Hash] a hash representation of this {MachHeader64} 736 | def to_h 737 | { 738 | "reserved" => reserved, 739 | }.merge super 740 | end 741 | end 742 | 743 | # Prelinked kernel/"kernelcache" header structure 744 | class PrelinkedKernelHeader < MachOStructure 745 | # @return [Integer] the magic number for a compressed header ({COMPRESSED_MAGIC}) 746 | field :signature, :uint32, :endian => :big 747 | 748 | # @return [Integer] the type of compression used 749 | field :compress_type, :uint32, :endian => :big 750 | 751 | # @return [Integer] a checksum for the uncompressed data 752 | field :adler32, :uint32, :endian => :big 753 | 754 | # @return [Integer] the size of the uncompressed data, in bytes 755 | field :uncompressed_size, :uint32, :endian => :big 756 | 757 | # @return [Integer] the size of the compressed data, in bytes 758 | field :compressed_size, :uint32, :endian => :big 759 | 760 | # @return [Integer] the version of the prelink format 761 | field :prelink_version, :uint32, :endian => :big 762 | 763 | # @return [void] 764 | field :reserved, :string, :size => 40, :unpack => "L>10" 765 | 766 | # @return [void] 767 | field :platform_name, :string, :size => 64 768 | 769 | # @return [void] 770 | field :root_path, :string, :size => 256 771 | 772 | # @return [Boolean] whether this prelinked kernel supports KASLR 773 | def kaslr? 774 | prelink_version >= 1 775 | end 776 | 777 | # @return [Boolean] whether this prelinked kernel is compressed with LZSS 778 | def lzss? 779 | compress_type == COMP_TYPE_LZSS 780 | end 781 | 782 | # @return [Boolean] whether this prelinked kernel is compressed with LZVN 783 | def lzvn? 784 | compress_type == COMP_TYPE_FASTLIB 785 | end 786 | 787 | # @return [Hash] a hash representation of this {PrelinkedKernelHeader} 788 | def to_h 789 | { 790 | "signature" => signature, 791 | "compress_type" => compress_type, 792 | "adler32" => adler32, 793 | "uncompressed_size" => uncompressed_size, 794 | "compressed_size" => compressed_size, 795 | "prelink_version" => prelink_version, 796 | "reserved" => reserved, 797 | "platform_name" => platform_name, 798 | "root_path" => root_path, 799 | }.merge super 800 | end 801 | end 802 | end 803 | end 804 | --------------------------------------------------------------------------------