├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── docker-bake.hcl ├── example ├── brittany │ └── Dockerfile ├── clang-format │ └── Dockerfile └── patchelf │ └── Dockerfile ├── rust-toolchain.toml └── src ├── action.rs ├── action ├── bundle_dynamic_dependencies.rs ├── bundle_executable.rs ├── bundle_shared_object_dependencies.rs ├── compress_executable.rs ├── emit.rs ├── exclude_glob.rs ├── include_glob.rs ├── make_directory.rs └── test.rs ├── base.rs ├── base ├── error.rs ├── log.rs └── trace.rs ├── bin └── main.rs ├── domain.rs ├── domain ├── bundle.rs ├── bundle_path.rs ├── executable.rs ├── executable │ ├── resolver.rs │ └── search_paths.rs ├── jail.rs └── resource.rs └── magicpak.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: ['*'] 4 | 5 | name: Release 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-22.04 11 | strategy: 12 | matrix: 13 | target: 14 | - x86_64-unknown-linux-musl 15 | - aarch64-unknown-linux-musl 16 | env: 17 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc 18 | steps: 19 | - uses: actions/checkout@v3 20 | - run: sudo apt-get update 21 | - run: sudo apt-get install -y musl-tools busybox-static 22 | - run: sudo apt-get install -y gcc-aarch64-linux-gnu 23 | if: matrix.target == 'aarch64-unknown-linux-musl' 24 | - run: rustup target add ${{ matrix.target }} 25 | - uses: actions/cache@v3 26 | with: 27 | path: | 28 | target/ 29 | ~/.cargo/git/db/ 30 | ~/.cargo/registry/cache/ 31 | ~/.cargo/registry/index/ 32 | key: "${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}" 33 | - run: cargo build --release --all-features --target=${{ matrix.target }} --locked 34 | - uses: actions/upload-artifact@v3 35 | with: 36 | name: musl-executable-${{ matrix.target }} 37 | path: ./target/${{ matrix.target }}/release/magicpak 38 | - run: cargo test --release --all-features --target=${{ matrix.target }} 39 | if: matrix.target == 'x86_64-unknown-linux-musl' # TODO: test aarch64 40 | build_docker_images: 41 | name: Build and push docker images 42 | runs-on: ubuntu-22.04 43 | needs: test 44 | steps: 45 | - name: Free up disk space 46 | run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android 47 | - uses: actions/checkout@v3 48 | - uses: actions/download-artifact@v3 49 | with: 50 | name: musl-executable-x86_64-unknown-linux-musl 51 | - run: mkdir -p dist/amd64 && mv ./magicpak ./dist/amd64/magicpak 52 | - uses: actions/download-artifact@v3 53 | with: 54 | name: musl-executable-aarch64-unknown-linux-musl 55 | - run: mkdir -p dist/arm64 && mv ./magicpak ./dist/arm64/magicpak 56 | - uses: docker/setup-qemu-action@v2 57 | with: 58 | platforms: arm64 59 | - uses: docker/login-action@v2 60 | with: 61 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 62 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 63 | - uses: docker/setup-buildx-action@v2 64 | - run: docker buildx bake --push --set '*.cache-from=type=gha' --set '*.cache-to=type=gha,mode=max' 65 | env: 66 | BIN_DIR: "./dist" 67 | VERSION: "1.4.0" 68 | release: 69 | name: Release 70 | runs-on: ubuntu-22.04 71 | needs: build_docker_images 72 | strategy: 73 | matrix: 74 | target: 75 | - x86_64-unknown-linux-musl 76 | - aarch64-unknown-linux-musl 77 | steps: 78 | - uses: actions/download-artifact@v3 79 | with: 80 | name: musl-executable-${{ matrix.target }} 81 | - run: mv magicpak magicpak-${{ matrix.target }} 82 | - uses: softprops/action-gh-release@v1 83 | with: 84 | files: magicpak-* 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | upload: 88 | name: Upload to crates.io 89 | runs-on: ubuntu-22.04 90 | needs: release 91 | steps: 92 | - uses: actions/checkout@v3 93 | - run: cargo publish 94 | env: 95 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 96 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Test and Lint 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | matrix: 11 | target: 12 | - x86_64-unknown-linux-musl 13 | - aarch64-unknown-linux-musl 14 | env: 15 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc 16 | steps: 17 | - uses: actions/checkout@v3 18 | - run: sudo apt-get update 19 | - run: sudo apt-get install -y musl-tools busybox-static 20 | - run: sudo apt-get install -y gcc-aarch64-linux-gnu 21 | if: matrix.target == 'aarch64-unknown-linux-musl' 22 | - run: rustup target add ${{ matrix.target }} 23 | - uses: actions/cache@v3 24 | with: 25 | path: | 26 | target/ 27 | ~/.cargo/git/db/ 28 | ~/.cargo/registry/cache/ 29 | ~/.cargo/registry/index/ 30 | key: "${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}" 31 | - run: cargo build --release --all-features --target=${{ matrix.target }} --locked 32 | - uses: actions/upload-artifact@v3 33 | with: 34 | name: musl-executable-${{ matrix.target }} 35 | path: ./target/${{ matrix.target }}/release/magicpak 36 | - run: cargo test --release --all-features --target=${{ matrix.target }} 37 | if: matrix.target == 'x86_64-unknown-linux-musl' # TODO: test aarch64 38 | fmt: 39 | name: Rustfmt 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v3 43 | - run: cargo fmt --all -- --check 44 | clippy: 45 | name: Clippy 46 | runs-on: ubuntu-22.04 47 | steps: 48 | - uses: actions/checkout@v3 49 | - run: cargo clippy --all-features -- -D warnings 50 | test_examples: 51 | name: Test examples 52 | runs-on: ubuntu-22.04 53 | needs: test 54 | steps: 55 | - uses: actions/checkout@v3 56 | - uses: actions/download-artifact@v3 57 | with: 58 | name: musl-executable-x86_64-unknown-linux-musl 59 | - run: mkdir -p dist/amd64 && mv ./magicpak ./dist/amd64/magicpak 60 | - uses: docker/setup-buildx-action@v2 61 | - name: Build examples 62 | run: docker buildx bake --set '*.cache-from=type=gha' --set '*.cache-to=type=gha,mode=max' --set 'base.platform=linux/amd64' example 63 | env: 64 | BIN_DIR: ./dist 65 | VERSION: 1.4.0 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | docker-bake.override.hcl 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.4.0] - 2022-12-12 6 | 7 | - Ignore already emitted symlinks ([#49](https://github.com/coord-e/magicpak/pull/49)) 8 | - Enable to supply multiple inputs ([#50](https://github.com/coord-e/magicpak/pull/50)) 9 | - Add noload resolver to avoid loading with dlopen(3) ([#51](https://github.com/coord-e/magicpak/pull/51)) 10 | - Dependency updates 11 | 12 | ## [1.3.2] - 2022-11-23 13 | 14 | - Fix not to canonicalize paths in `--include` ([#20](https://github.com/coord-e/magicpak/pull/20)) 15 | - Search statically linked dependencies of ELF objects specified by `--include` ([#33](https://github.com/coord-e/magicpak/pull/33)) 16 | - To deal with getaddrinfo(3) issue described in ([#12](https://github.com/coord-e/magicpak/issues/12)) 17 | - Improve error messages ([#36](https://github.com/coord-e/magicpak/pull/36)) 18 | - Dependency updates 19 | 20 | ## [1.3.1] - 2022-06-19 21 | 22 | - AArch64 support ([#14](https://github.com/coord-e/magicpak/pull/14)) 23 | - Use docker buildx bake to build container images ([#15](https://github.com/coord-e/magicpak/pull/15)) 24 | 25 | ## [1.3.0] - 2022-01-11 26 | 27 | - Fix busybox_jail_path file permissions ([#6](https://github.com/coord-e/magicpak/pull/6)) 28 | - Update and renew dependencies ([#7](https://github.com/coord-e/magicpak/pull/7)) 29 | - Fix usage of ExitStatus::from_raw and remove Error::DynamicSignaled ([#9](https://github.com/coord-e/magicpak/pull/9)) 30 | - Several CI fixes ([#8](https://github.com/coord-e/magicpak/pull/8), [#10](https://github.com/coord-e/magicpak/pull/10)) 31 | - This changed how `busybox` installed in the container images 32 | 33 | ## [1.2.0] - 2021-01-11 34 | 35 | - Fixed infinite recursion caused by mutually dependent shared libraries. (#[3](https://github.com/coord-e/magicpak/pulls/3)) 36 | - Fixed Clippy warnings. (#[4](https://github.com/coord-e/magicpak/pulls/4)) 37 | - Updated dependencies. 38 | - Changed how magicpak images are tagged. 39 | 40 | ## [1.1.0] - 2020-05-05 41 | 42 | - Fixed the order of `-ldl` option in resolver compilation. (#[1](https://github.com/coord-e/magicpak/pulls/1)) 43 | 44 | ## [1.0.3] - 2020-04-14 45 | 46 | - Fixed `--test` behavior when the resulting bundle contains `/bin/`. 47 | 48 | ## [1.0.2] - 2020-04-14 49 | 50 | - Fixed `--compress` when the executable is symlinked. 51 | - Added many test cases. 52 | 53 | ## [1.0.1] - 2020-04-11 54 | 55 | - Fixed a problem on CI. 56 | 57 | ## [1.0.0] - 2020-04-11 58 | 59 | - Added detailed explanation of usage to README. 60 | - Fixed bundled executable path when it is symlinked. 61 | 62 | ## [0.1.0] - 2020-04-03 63 | 64 | - Initial release. 65 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.15" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstyle" 16 | version = "1.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 19 | 20 | [[package]] 21 | name = "assert_cmd" 22 | version = "2.0.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151" 25 | dependencies = [ 26 | "anstyle", 27 | "bstr 1.0.1", 28 | "doc-comment", 29 | "predicates", 30 | "predicates-core", 31 | "predicates-tree", 32 | "wait-timeout", 33 | ] 34 | 35 | [[package]] 36 | name = "assert_fs" 37 | version = "1.0.13" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" 40 | dependencies = [ 41 | "anstyle", 42 | "doc-comment", 43 | "globwalk", 44 | "predicates", 45 | "predicates-core", 46 | "predicates-tree", 47 | "tempfile", 48 | ] 49 | 50 | [[package]] 51 | name = "autocfg" 52 | version = "1.0.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "1.2.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 61 | 62 | [[package]] 63 | name = "bstr" 64 | version = "0.2.14" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" 67 | dependencies = [ 68 | "memchr", 69 | ] 70 | 71 | [[package]] 72 | name = "bstr" 73 | version = "1.0.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" 76 | dependencies = [ 77 | "memchr", 78 | "once_cell", 79 | "regex-automata", 80 | "serde", 81 | ] 82 | 83 | [[package]] 84 | name = "cc" 85 | version = "1.0.77" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" 88 | 89 | [[package]] 90 | name = "cfg-if" 91 | version = "1.0.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 94 | 95 | [[package]] 96 | name = "clap" 97 | version = "4.1.14" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "906f7fe1da4185b7a282b2bc90172a496f9def1aca4545fe7526810741591e14" 100 | dependencies = [ 101 | "clap_builder", 102 | "clap_derive", 103 | "once_cell", 104 | ] 105 | 106 | [[package]] 107 | name = "clap_builder" 108 | version = "4.1.14" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "351f9ad9688141ed83dfd8f5fb998a06225ef444b48ff4dc43de6d409b7fd10b" 111 | dependencies = [ 112 | "bitflags", 113 | "clap_lex", 114 | "is-terminal", 115 | "strsim", 116 | "termcolor", 117 | ] 118 | 119 | [[package]] 120 | name = "clap_derive" 121 | version = "4.1.14" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "81d7dc0031c3a59a04fc2ba395c8e2dd463cba1859275f065d225f6122221b45" 124 | dependencies = [ 125 | "heck", 126 | "proc-macro2", 127 | "quote", 128 | "syn 2.0.15", 129 | ] 130 | 131 | [[package]] 132 | name = "clap_lex" 133 | version = "0.4.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" 136 | 137 | [[package]] 138 | name = "crossbeam-utils" 139 | version = "0.8.7" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" 142 | dependencies = [ 143 | "cfg-if", 144 | "lazy_static", 145 | ] 146 | 147 | [[package]] 148 | name = "crt0stack" 149 | version = "0.1.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "9274b445ee572d50bdeb17a1101be829becc565b5c12b21a697af4d360b48e8d" 152 | 153 | [[package]] 154 | name = "difflib" 155 | version = "0.4.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 158 | 159 | [[package]] 160 | name = "doc-comment" 161 | version = "0.3.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 164 | 165 | [[package]] 166 | name = "either" 167 | version = "1.6.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 170 | 171 | [[package]] 172 | name = "errno" 173 | version = "0.2.8" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 176 | dependencies = [ 177 | "errno-dragonfly", 178 | "libc", 179 | "winapi", 180 | ] 181 | 182 | [[package]] 183 | name = "errno" 184 | version = "0.3.1" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 187 | dependencies = [ 188 | "errno-dragonfly", 189 | "libc", 190 | "windows-sys 0.48.0", 191 | ] 192 | 193 | [[package]] 194 | name = "errno-dragonfly" 195 | version = "0.1.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 198 | dependencies = [ 199 | "cc", 200 | "libc", 201 | ] 202 | 203 | [[package]] 204 | name = "fastrand" 205 | version = "1.7.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 208 | dependencies = [ 209 | "instant", 210 | ] 211 | 212 | [[package]] 213 | name = "float-cmp" 214 | version = "0.9.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 217 | dependencies = [ 218 | "num-traits", 219 | ] 220 | 221 | [[package]] 222 | name = "fnv" 223 | version = "1.0.7" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 226 | 227 | [[package]] 228 | name = "glob" 229 | version = "0.3.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 232 | 233 | [[package]] 234 | name = "globset" 235 | version = "0.4.6" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" 238 | dependencies = [ 239 | "aho-corasick", 240 | "bstr 0.2.14", 241 | "fnv", 242 | "log", 243 | "regex", 244 | ] 245 | 246 | [[package]] 247 | name = "globwalk" 248 | version = "0.8.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 251 | dependencies = [ 252 | "bitflags", 253 | "ignore", 254 | "walkdir", 255 | ] 256 | 257 | [[package]] 258 | name = "goblin" 259 | version = "0.6.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" 262 | dependencies = [ 263 | "log", 264 | "plain", 265 | "scroll", 266 | ] 267 | 268 | [[package]] 269 | name = "heck" 270 | version = "0.4.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 273 | 274 | [[package]] 275 | name = "hermit-abi" 276 | version = "0.2.6" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 279 | dependencies = [ 280 | "libc", 281 | ] 282 | 283 | [[package]] 284 | name = "ignore" 285 | version = "0.4.17" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" 288 | dependencies = [ 289 | "crossbeam-utils", 290 | "globset", 291 | "lazy_static", 292 | "log", 293 | "memchr", 294 | "regex", 295 | "same-file", 296 | "thread_local", 297 | "walkdir", 298 | "winapi-util", 299 | ] 300 | 301 | [[package]] 302 | name = "instant" 303 | version = "0.1.12" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 306 | dependencies = [ 307 | "cfg-if", 308 | ] 309 | 310 | [[package]] 311 | name = "io-lifetimes" 312 | version = "1.0.3" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 315 | dependencies = [ 316 | "libc", 317 | "windows-sys 0.42.0", 318 | ] 319 | 320 | [[package]] 321 | name = "is-terminal" 322 | version = "0.4.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" 325 | dependencies = [ 326 | "hermit-abi", 327 | "io-lifetimes", 328 | "rustix 0.36.4", 329 | "windows-sys 0.42.0", 330 | ] 331 | 332 | [[package]] 333 | name = "itertools" 334 | version = "0.10.3" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 337 | dependencies = [ 338 | "either", 339 | ] 340 | 341 | [[package]] 342 | name = "lazy_static" 343 | version = "1.4.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 346 | 347 | [[package]] 348 | name = "libc" 349 | version = "0.2.137" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 352 | 353 | [[package]] 354 | name = "linux-raw-sys" 355 | version = "0.1.3" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" 358 | 359 | [[package]] 360 | name = "linux-raw-sys" 361 | version = "0.3.7" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" 364 | 365 | [[package]] 366 | name = "log" 367 | version = "0.4.14" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 370 | dependencies = [ 371 | "cfg-if", 372 | ] 373 | 374 | [[package]] 375 | name = "magicpak" 376 | version = "1.4.0" 377 | dependencies = [ 378 | "assert_cmd", 379 | "assert_fs", 380 | "clap", 381 | "crt0stack", 382 | "glob", 383 | "goblin", 384 | "nix", 385 | "predicates", 386 | "tempfile", 387 | "tracing", 388 | "tracing-subscriber", 389 | "which", 390 | ] 391 | 392 | [[package]] 393 | name = "memchr" 394 | version = "2.4.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 397 | 398 | [[package]] 399 | name = "memoffset" 400 | version = "0.7.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 403 | dependencies = [ 404 | "autocfg", 405 | ] 406 | 407 | [[package]] 408 | name = "nix" 409 | version = "0.26.2" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 412 | dependencies = [ 413 | "bitflags", 414 | "cfg-if", 415 | "libc", 416 | "memoffset", 417 | "pin-utils", 418 | "static_assertions", 419 | ] 420 | 421 | [[package]] 422 | name = "normalize-line-endings" 423 | version = "0.3.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 426 | 427 | [[package]] 428 | name = "nu-ansi-term" 429 | version = "0.46.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 432 | dependencies = [ 433 | "overload", 434 | "winapi", 435 | ] 436 | 437 | [[package]] 438 | name = "num-traits" 439 | version = "0.2.14" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 442 | dependencies = [ 443 | "autocfg", 444 | ] 445 | 446 | [[package]] 447 | name = "once_cell" 448 | version = "1.16.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 451 | 452 | [[package]] 453 | name = "overload" 454 | version = "0.1.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 457 | 458 | [[package]] 459 | name = "pin-project-lite" 460 | version = "0.2.9" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 463 | 464 | [[package]] 465 | name = "pin-utils" 466 | version = "0.1.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 469 | 470 | [[package]] 471 | name = "plain" 472 | version = "0.2.3" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 475 | 476 | [[package]] 477 | name = "predicates" 478 | version = "3.0.3" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" 481 | dependencies = [ 482 | "anstyle", 483 | "difflib", 484 | "float-cmp", 485 | "itertools", 486 | "normalize-line-endings", 487 | "predicates-core", 488 | "regex", 489 | ] 490 | 491 | [[package]] 492 | name = "predicates-core" 493 | version = "1.0.6" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 496 | 497 | [[package]] 498 | name = "predicates-tree" 499 | version = "1.0.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "aee95d988ee893cb35c06b148c80ed2cd52c8eea927f50ba7a0be1a786aeab73" 502 | dependencies = [ 503 | "predicates-core", 504 | "treeline", 505 | ] 506 | 507 | [[package]] 508 | name = "proc-macro2" 509 | version = "1.0.56" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" 512 | dependencies = [ 513 | "unicode-ident", 514 | ] 515 | 516 | [[package]] 517 | name = "quote" 518 | version = "1.0.26" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 521 | dependencies = [ 522 | "proc-macro2", 523 | ] 524 | 525 | [[package]] 526 | name = "redox_syscall" 527 | version = "0.3.5" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 530 | dependencies = [ 531 | "bitflags", 532 | ] 533 | 534 | [[package]] 535 | name = "regex" 536 | version = "1.4.3" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" 539 | dependencies = [ 540 | "aho-corasick", 541 | "memchr", 542 | "regex-syntax", 543 | "thread_local", 544 | ] 545 | 546 | [[package]] 547 | name = "regex-automata" 548 | version = "0.1.10" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 551 | 552 | [[package]] 553 | name = "regex-syntax" 554 | version = "0.6.22" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" 557 | 558 | [[package]] 559 | name = "rustix" 560 | version = "0.36.4" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" 563 | dependencies = [ 564 | "bitflags", 565 | "errno 0.2.8", 566 | "io-lifetimes", 567 | "libc", 568 | "linux-raw-sys 0.1.3", 569 | "windows-sys 0.42.0", 570 | ] 571 | 572 | [[package]] 573 | name = "rustix" 574 | version = "0.37.3" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "62b24138615de35e32031d041a09032ef3487a616d901ca4db224e7d557efae2" 577 | dependencies = [ 578 | "bitflags", 579 | "errno 0.3.1", 580 | "io-lifetimes", 581 | "libc", 582 | "linux-raw-sys 0.3.7", 583 | "windows-sys 0.45.0", 584 | ] 585 | 586 | [[package]] 587 | name = "same-file" 588 | version = "1.0.6" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 591 | dependencies = [ 592 | "winapi-util", 593 | ] 594 | 595 | [[package]] 596 | name = "scroll" 597 | version = "0.11.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" 600 | dependencies = [ 601 | "scroll_derive", 602 | ] 603 | 604 | [[package]] 605 | name = "scroll_derive" 606 | version = "0.11.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e" 609 | dependencies = [ 610 | "proc-macro2", 611 | "quote", 612 | "syn 1.0.103", 613 | ] 614 | 615 | [[package]] 616 | name = "serde" 617 | version = "1.0.147" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" 620 | 621 | [[package]] 622 | name = "sharded-slab" 623 | version = "0.1.4" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 626 | dependencies = [ 627 | "lazy_static", 628 | ] 629 | 630 | [[package]] 631 | name = "smallvec" 632 | version = "1.10.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 635 | 636 | [[package]] 637 | name = "static_assertions" 638 | version = "1.1.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 641 | 642 | [[package]] 643 | name = "strsim" 644 | version = "0.10.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 647 | 648 | [[package]] 649 | name = "syn" 650 | version = "1.0.103" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" 653 | dependencies = [ 654 | "proc-macro2", 655 | "quote", 656 | "unicode-ident", 657 | ] 658 | 659 | [[package]] 660 | name = "syn" 661 | version = "2.0.15" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" 664 | dependencies = [ 665 | "proc-macro2", 666 | "quote", 667 | "unicode-ident", 668 | ] 669 | 670 | [[package]] 671 | name = "tempfile" 672 | version = "3.5.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" 675 | dependencies = [ 676 | "cfg-if", 677 | "fastrand", 678 | "redox_syscall", 679 | "rustix 0.37.3", 680 | "windows-sys 0.45.0", 681 | ] 682 | 683 | [[package]] 684 | name = "termcolor" 685 | version = "1.1.2" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 688 | dependencies = [ 689 | "winapi-util", 690 | ] 691 | 692 | [[package]] 693 | name = "thread_local" 694 | version = "1.1.4" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 697 | dependencies = [ 698 | "once_cell", 699 | ] 700 | 701 | [[package]] 702 | name = "tracing" 703 | version = "0.1.37" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 706 | dependencies = [ 707 | "cfg-if", 708 | "pin-project-lite", 709 | "tracing-attributes", 710 | "tracing-core", 711 | ] 712 | 713 | [[package]] 714 | name = "tracing-attributes" 715 | version = "0.1.23" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" 718 | dependencies = [ 719 | "proc-macro2", 720 | "quote", 721 | "syn 1.0.103", 722 | ] 723 | 724 | [[package]] 725 | name = "tracing-core" 726 | version = "0.1.30" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 729 | dependencies = [ 730 | "once_cell", 731 | "valuable", 732 | ] 733 | 734 | [[package]] 735 | name = "tracing-log" 736 | version = "0.1.3" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 739 | dependencies = [ 740 | "lazy_static", 741 | "log", 742 | "tracing-core", 743 | ] 744 | 745 | [[package]] 746 | name = "tracing-subscriber" 747 | version = "0.3.17" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 750 | dependencies = [ 751 | "nu-ansi-term", 752 | "sharded-slab", 753 | "smallvec", 754 | "thread_local", 755 | "tracing-core", 756 | "tracing-log", 757 | ] 758 | 759 | [[package]] 760 | name = "treeline" 761 | version = "0.1.0" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" 764 | 765 | [[package]] 766 | name = "unicode-ident" 767 | version = "1.0.5" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 770 | 771 | [[package]] 772 | name = "valuable" 773 | version = "0.1.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 776 | 777 | [[package]] 778 | name = "wait-timeout" 779 | version = "0.2.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 782 | dependencies = [ 783 | "libc", 784 | ] 785 | 786 | [[package]] 787 | name = "walkdir" 788 | version = "2.3.1" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" 791 | dependencies = [ 792 | "same-file", 793 | "winapi", 794 | "winapi-util", 795 | ] 796 | 797 | [[package]] 798 | name = "which" 799 | version = "4.4.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" 802 | dependencies = [ 803 | "either", 804 | "libc", 805 | "once_cell", 806 | ] 807 | 808 | [[package]] 809 | name = "winapi" 810 | version = "0.3.9" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 813 | dependencies = [ 814 | "winapi-i686-pc-windows-gnu", 815 | "winapi-x86_64-pc-windows-gnu", 816 | ] 817 | 818 | [[package]] 819 | name = "winapi-i686-pc-windows-gnu" 820 | version = "0.4.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 823 | 824 | [[package]] 825 | name = "winapi-util" 826 | version = "0.1.5" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 829 | dependencies = [ 830 | "winapi", 831 | ] 832 | 833 | [[package]] 834 | name = "winapi-x86_64-pc-windows-gnu" 835 | version = "0.4.0" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 838 | 839 | [[package]] 840 | name = "windows-sys" 841 | version = "0.42.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 844 | dependencies = [ 845 | "windows_aarch64_gnullvm 0.42.2", 846 | "windows_aarch64_msvc 0.42.2", 847 | "windows_i686_gnu 0.42.2", 848 | "windows_i686_msvc 0.42.2", 849 | "windows_x86_64_gnu 0.42.2", 850 | "windows_x86_64_gnullvm 0.42.2", 851 | "windows_x86_64_msvc 0.42.2", 852 | ] 853 | 854 | [[package]] 855 | name = "windows-sys" 856 | version = "0.45.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 859 | dependencies = [ 860 | "windows-targets 0.42.2", 861 | ] 862 | 863 | [[package]] 864 | name = "windows-sys" 865 | version = "0.48.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 868 | dependencies = [ 869 | "windows-targets 0.48.0", 870 | ] 871 | 872 | [[package]] 873 | name = "windows-targets" 874 | version = "0.42.2" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 877 | dependencies = [ 878 | "windows_aarch64_gnullvm 0.42.2", 879 | "windows_aarch64_msvc 0.42.2", 880 | "windows_i686_gnu 0.42.2", 881 | "windows_i686_msvc 0.42.2", 882 | "windows_x86_64_gnu 0.42.2", 883 | "windows_x86_64_gnullvm 0.42.2", 884 | "windows_x86_64_msvc 0.42.2", 885 | ] 886 | 887 | [[package]] 888 | name = "windows-targets" 889 | version = "0.48.0" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 892 | dependencies = [ 893 | "windows_aarch64_gnullvm 0.48.0", 894 | "windows_aarch64_msvc 0.48.0", 895 | "windows_i686_gnu 0.48.0", 896 | "windows_i686_msvc 0.48.0", 897 | "windows_x86_64_gnu 0.48.0", 898 | "windows_x86_64_gnullvm 0.48.0", 899 | "windows_x86_64_msvc 0.48.0", 900 | ] 901 | 902 | [[package]] 903 | name = "windows_aarch64_gnullvm" 904 | version = "0.42.2" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 907 | 908 | [[package]] 909 | name = "windows_aarch64_gnullvm" 910 | version = "0.48.0" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 913 | 914 | [[package]] 915 | name = "windows_aarch64_msvc" 916 | version = "0.42.2" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 919 | 920 | [[package]] 921 | name = "windows_aarch64_msvc" 922 | version = "0.48.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 925 | 926 | [[package]] 927 | name = "windows_i686_gnu" 928 | version = "0.42.2" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 931 | 932 | [[package]] 933 | name = "windows_i686_gnu" 934 | version = "0.48.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 937 | 938 | [[package]] 939 | name = "windows_i686_msvc" 940 | version = "0.42.2" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 943 | 944 | [[package]] 945 | name = "windows_i686_msvc" 946 | version = "0.48.0" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 949 | 950 | [[package]] 951 | name = "windows_x86_64_gnu" 952 | version = "0.42.2" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 955 | 956 | [[package]] 957 | name = "windows_x86_64_gnu" 958 | version = "0.48.0" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 961 | 962 | [[package]] 963 | name = "windows_x86_64_gnullvm" 964 | version = "0.42.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 967 | 968 | [[package]] 969 | name = "windows_x86_64_gnullvm" 970 | version = "0.48.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 973 | 974 | [[package]] 975 | name = "windows_x86_64_msvc" 976 | version = "0.42.2" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 979 | 980 | [[package]] 981 | name = "windows_x86_64_msvc" 982 | version = "0.48.0" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 985 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "magicpak" 3 | version = "1.4.0" 4 | authors = ["coord_e "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "Build minimal docker images without static linking" 8 | homepage = "https://github.com/coord-e/magicpak" 9 | repository = "https://github.com/coord-e/magicpak" 10 | keywords = ["docker"] 11 | categories = ["development-tools"] 12 | 13 | [[bin]] 14 | name = "magicpak" 15 | path = "src/bin/main.rs" 16 | 17 | [lib] 18 | name = "magicpak" 19 | path = "src/magicpak.rs" 20 | 21 | [dependencies] 22 | clap = { version = "4", features = ["env", "derive"] } 23 | crt0stack = "0.1" 24 | glob = "0.3.1" 25 | goblin = "0.6" 26 | nix = "0.26" 27 | tempfile = "3.5.0" 28 | tracing = "0.1" 29 | tracing-subscriber = "0.3" 30 | which = "4.4" 31 | 32 | [dev-dependencies] 33 | assert_cmd = "2" 34 | assert_fs = "1.0.13" 35 | predicates = "3" 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE 2 | FROM ${BASE_IMAGE} 3 | 4 | ARG APT_PACKAGES 5 | RUN DEBIAN_FRONTEND=noninteractive \ 6 | apt-get update -y \ 7 | && apt-get install -y --no-install-recommends gcc libc-dev xz-utils busybox-static ${APT_PACKAGES} \ 8 | && apt-get clean \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | ARG UPX_VERSION 12 | ARG MAGICPAK_DIR 13 | ARG TARGETARCH 14 | 15 | ADD https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz /tmp/upx.tar.xz 16 | RUN cd /tmp \ 17 | && tar --strip-components=1 -xf upx.tar.xz \ 18 | && mv upx /bin/ \ 19 | && rm upx.tar.xz 20 | 21 | COPY $MAGICPAK_DIR/$TARGETARCH/magicpak /bin/magicpak 22 | RUN chmod +x /bin/magicpak 23 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # magicpak 2 | 3 | [![Actions Status](https://github.com/coord-e/magicpak/workflows/Test%20and%20Lint/badge.svg)](https://github.com/coord-e/magicpak/actions?workflow=Test+and+Lint) 4 | [![Actions Status](https://github.com/coord-e/magicpak/workflows/Release/badge.svg)](https://github.com/coord-e/magicpak/actions?workflow=Release) 5 | [![License](https://img.shields.io/crates/l/magicpak)](https://crates.io/crates/magicpak) 6 | 7 | `magicpak` enables you to build minimal docker images without any bothersome preparation such as static linking. 8 | 9 | ```dockerfile 10 | # You prepare /bin/your_executable here... 11 | 12 | ADD https://github.com/coord-e/magicpak/releases/download/v1.4.0/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak 13 | RUN chmod +x /usr/bin/magicpak 14 | 15 | RUN /usr/bin/magicpak -v /bin/your_executable /bundle 16 | 17 | FROM scratch 18 | COPY --from=0 /bundle /. 19 | 20 | CMD ["/bin/your_executable"] 21 | ``` 22 | 23 | That's it! The resulting image shall only contain what your executable requires at runtime. You can find more useful examples of `magicpak` under [example/](/example). 24 | 25 | ## Feature 26 | 27 | `magicpak` is a command-line utility that analyzes and bundles runtime dependencies of the executable. `magicpak` basically collects all shared object dependencies that are required by a dynamic linker at runtime. Additionally, `magicpak`'s contributions are summarized as follows: 28 | 29 | - **Simple**. You can build a minimal image just by adding a few lines to your `Dockerfile`. 30 | - **Full-featured**. You can bundle, test, and compress your executable at once. You can focus on your business because `magicpak` handles all `Dockerfile`-specific matters to decrease image size. 31 | - **Dynamic analysis**. `--dynamic` flag enables a dynamic analysis that can discover dependencies other than dynamically linked libraries. 32 | - **Flexible**. We expose a full control of resulting bundle with a family of options like `--include` and `--exclude`. You can deal with dependencies that cannot be detected automatically. 33 | - **Stable**. We don't parse undocumented and sometimes inaccurate ldd(1) outputs. Instead, we use dlopen(3) and dlinfo(3) in glibc to query shared library locations to ld.so(8). 34 | 35 | `magicpak` is especially useful when you find it difficult to produce a statically linked executable. Also, `magicpak` is powerful when building from source is bothering or the source code is not public, because `magicpak` only requires the executable to build a minimal docker image. 36 | 37 | ## Usage 38 | 39 | You can start with `magicpak path/to/executable path/to/output`. This simply analyzes runtime dependencies of your executable statically and put everything your executable needs in runtime to the specified output directory. Once they've bundled, we can simply copy them to the `scratch` image in the second stage as follows. 40 | 41 | ```dockerfile 42 | RUN magicpak path/to/executable /bundle 43 | 44 | FROM scratch 45 | COPY --from=0 /bundle /. 46 | ``` 47 | 48 | Some executables work well in this way. However, others fail to run properly because `magicpak`'s static analysis isn't enough to detect all files needed by them at runtime. For this case, `magicpak` has `--include ` option to specify the missing requirements manually. Moreover, you can use `--dynamic` to automatically include files that are accessed by the executable during execution. 49 | 50 | Despite our careful implementation, our analysis is unreliable in a way because we can't completely determine the runtime behavior before its execution. To ensure that `magicpak` collected all dependencies to perform a specific task, `--test` option is implemented. `--test` enables testing of the resulting bundle using chroot(2). 51 | 52 | The size of the resulting image is our main concern. `magicpak` supports executable compression using `upx`. You can enable it with `--compress`. 53 | 54 | ### Supported options 55 | 56 | ``` 57 | Usage: magicpak [OPTIONS] ... 58 | 59 | Arguments: 60 | ... Input executable 61 | Output destination 62 | 63 | Options: 64 | -i, --include Additionally include files/directories with glob patterns 65 | -e, --exclude Exclude files/directories from the resulting bundle with glob patterns 66 | --mkdir Make directories in the resulting bundle 67 | -r, --install-to Specify the installation path of the executable in the bundle 68 | --log-level Specify the log level [default: Warn] [possible values: Off, Error, Warn, Info, Debug] 69 | -v, --verbose Verbose mode, same as --log-level Info 70 | -t, --test Enable testing 71 | --test-command Specify the test command to use in --test 72 | --test-stdin Specify stdin content supplied to the test command in --test 73 | --test-stdout Test stdout of the test command 74 | -d, --dynamic Enable dynamic analysis 75 | --dynamic-arg Specify arguments passed to the executable in --dynamic 76 | --dynamic-stdin Specify stdin content supplied to the executable in --dynamic 77 | -c, --compress Compress the executable with npx 78 | --upx-arg Specify arguments passed to upx in --compress 79 | --busybox Specify the path or name of busybox that would be used in testing [default: busybox] 80 | --upx Specify the path or name of upx that would be used in compression [default: upx] 81 | --cc Specify the path or name of c compiler that would be used in the name resolution of shared library dependencies [env: CC=] [default: cc] 82 | --experimental-noload-resolver [EXPERIMENTAL] Resolve dynamic library paths without loading in dlopen(3) 83 | -h, --help Print help information 84 | ``` 85 | 86 | ### Docker images 87 | 88 | We provide some base images that contain `magicpak` and its optional dependencies to get started. 89 | 90 | | name | description | 91 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 92 | | [magicpak/debian ![magicpak/debian](https://img.shields.io/docker/pulls/magicpak/debian)](https://hub.docker.com/r/magicpak/debian) | [library/debian](http://hub.docker.com/_/debian) with `magicpak` | 93 | | [magicpak/cc ![magicpak/cc](https://img.shields.io/docker/pulls/magicpak/cc)](https://hub.docker.com/r/magicpak/cc) | [library/debian](http://hub.docker.com/_/debian) with `build-essential`, `clang`, and `magicpak` | 94 | | [magicpak/haskell ![magicpak/haskell](https://img.shields.io/docker/pulls/magicpak/haskell)](https://hub.docker.com/r/magicpak/haskell) | [library/haskell](http://hub.docker.com/_/haskell) with `magicpak` | 95 | | [magicpak/rust ![magicpak/rust](https://img.shields.io/docker/pulls/magicpak/rust)](https://hub.docker.com/r/magicpak/rust) | [library/rust](http://hub.docker.com/_/rust) with `magicpak` | 96 | 97 | ### Example 98 | 99 | The following is a dockerfile using `magicpak` for a docker image of [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html), a formatter for C/C++/etc. ([example/clang-format](/example/clang-format)) 100 | 101 | ```dockerfile 102 | FROM magicpak/debian:buster-magicpak1.4.0 103 | 104 | RUN apt-get -y update 105 | RUN apt-get -y --no-install-recommends install clang-format 106 | 107 | RUN magicpak $(which clang-format) /bundle -v \ 108 | --compress \ 109 | --upx-arg --best \ 110 | --test \ 111 | --test-stdin "int main( ){ }" \ 112 | --test-stdout "int main() {}" \ 113 | --install-to /bin/ 114 | 115 | FROM scratch 116 | COPY --from=0 /bundle /. 117 | 118 | WORKDIR /workdir 119 | 120 | CMD ["/bin/clang-format"] 121 | ``` 122 | 123 | ### Note on name resolution and glibc 124 | 125 | If your program uses glibc for name resolution (most likely it does), the call to getaddrinfo(3) will result in an error after bundled by magicpak. 126 | This can be resolved by manually including the NSS-related shared libraries as shown below. 127 | 128 | ```dockerfile 129 | # example on x86_64 Debian-based image: 130 | RUN magicpak path/to/executable /bundle --include '/lib/x86_64-linux-gnu/libnss_*' 131 | ``` 132 | 133 | ### Note on jemalloc 134 | 135 | If your program depends on libjemalloc, magicpak may fail with the following message. 136 | 137 | ``` 138 | error: Unable to lookup shared library: /lib/aarch64-linux-gnu/libjemalloc.so.2: cannot allocate memory in static TLS block 139 | ``` 140 | 141 | You can use `--experimental-noload-resolver` flag to workaround this. See [#19](https://github.com/coord-e/magicpak/issues/19) for details. 142 | 143 | ## Disclaimer 144 | 145 | `magicpak` comes with absolutely no warranty. There's no guarantee that the processed bundle works properly and identically to the original executable. Although I had no problem using `magicpak` for building various kinds of images, it is recommended to use this with caution and make a careful examination of the resulting bundle. 146 | 147 | ## License 148 | 149 | Licensed under either of 150 | 151 | * Apache License, Version 2.0 152 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 153 | * MIT license 154 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 155 | 156 | at your option. 157 | 158 | ## Contribution 159 | 160 | Unless you explicitly state otherwise, any contribution intentionally submitted 161 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 162 | dual licensed as above, without any additional terms or conditions. 163 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | variable "VERSION" {} 2 | 3 | variable "BIN_DIR" {} 4 | 5 | variable "IMAGE_PREFIX" { 6 | default = ["docker.io/magicpak/"] 7 | } 8 | 9 | variable "UPX_VERSION" { 10 | default = "3.96" 11 | } 12 | 13 | target "base" { 14 | dockerfile = "Dockerfile" 15 | platforms = [ 16 | "linux/amd64", 17 | "linux/arm64", 18 | ] 19 | args = { 20 | MAGICPAK_DIR = BIN_DIR 21 | UPX_VERSION = UPX_VERSION 22 | APT_PACKAGES = "" 23 | } 24 | } 25 | 26 | function "tags_for" { 27 | params = [name, tag] 28 | result = concat( 29 | formatlist("%s${name}:${tag}", IMAGE_PREFIX), 30 | formatlist("%s${name}:%s", IMAGE_PREFIX, equal(tag, "latest") ? "magicpak${VERSION}" : "${tag}-magicpak${VERSION}"), 31 | ) 32 | } 33 | 34 | group "default" { 35 | targets = [ 36 | "debian", 37 | "rust", 38 | "cc", 39 | "haskell", 40 | ] 41 | } 42 | 43 | group "debian" { 44 | targets = [ 45 | "debian-latest", 46 | "debian-bullseye", 47 | "debian-buster", 48 | "debian-stretch", 49 | ] 50 | } 51 | 52 | target "debian-latest" { 53 | inherits = ["base"] 54 | tags = tags_for("debian", "latest") 55 | args = { 56 | BASE_IMAGE = "debian:latest" 57 | } 58 | } 59 | 60 | target "debian-bullseye" { 61 | inherits = ["base"] 62 | tags = tags_for("debian", "bullseye") 63 | args = { 64 | BASE_IMAGE = "debian:bullseye" 65 | } 66 | } 67 | 68 | target "debian-buster" { 69 | inherits = ["base"] 70 | tags = tags_for("debian", "buster") 71 | args = { 72 | BASE_IMAGE = "debian:buster" 73 | } 74 | } 75 | 76 | target "debian-stretch" { 77 | inherits = ["base"] 78 | tags = tags_for("debian", "stretch") 79 | args = { 80 | BASE_IMAGE = "debian:stretch" 81 | } 82 | } 83 | 84 | group "rust" { 85 | targets = [ 86 | "rust-latest", 87 | "rust-1", 88 | "rust-149", 89 | ] 90 | } 91 | 92 | target "rust-latest" { 93 | inherits = ["base"] 94 | tags = tags_for("rust", "latest") 95 | args = { 96 | BASE_IMAGE = "rust:latest" 97 | } 98 | } 99 | 100 | target "rust-1" { 101 | inherits = ["base"] 102 | tags = tags_for("rust", "1") 103 | args = { 104 | BASE_IMAGE = "rust:1" 105 | } 106 | } 107 | 108 | target "rust-149" { 109 | inherits = ["base"] 110 | tags = tags_for("rust", "1.49") 111 | args = { 112 | BASE_IMAGE = "rust:1.49" 113 | } 114 | } 115 | 116 | group "cc" { 117 | targets = [ 118 | "cc-latest", 119 | "cc-10", 120 | "cc-9", 121 | "cc-8", 122 | ] 123 | } 124 | 125 | target "cc-latest" { 126 | inherits = ["base"] 127 | tags = tags_for("cc", "latest") 128 | args = { 129 | BASE_IMAGE = "gcc:latest" 130 | APT_PACKAGES = "build-essential clang" 131 | } 132 | } 133 | 134 | target "cc-10" { 135 | inherits = ["base"] 136 | tags = tags_for("cc", "10") 137 | args = { 138 | BASE_IMAGE = "gcc:10" 139 | APT_PACKAGES = "build-essential clang" 140 | } 141 | } 142 | 143 | target "cc-9" { 144 | inherits = ["base"] 145 | tags = tags_for("cc", "9") 146 | args = { 147 | BASE_IMAGE = "gcc:9" 148 | APT_PACKAGES = "build-essential clang" 149 | } 150 | } 151 | 152 | target "cc-8" { 153 | inherits = ["base"] 154 | tags = tags_for("cc", "8") 155 | args = { 156 | BASE_IMAGE = "gcc:8" 157 | APT_PACKAGES = "build-essential clang" 158 | } 159 | } 160 | 161 | group "haskell" { 162 | targets = [ 163 | "haskell-latest", 164 | "haskell-8", 165 | "haskell-810", 166 | "haskell-8102", 167 | "haskell-88", 168 | "haskell-86", 169 | ] 170 | } 171 | 172 | target "haskell-latest" { 173 | inherits = ["base"] 174 | tags = tags_for("haskell", "latest") 175 | args = { 176 | BASE_IMAGE = "haskell:latest" 177 | } 178 | } 179 | 180 | target "haskell-8" { 181 | inherits = ["base"] 182 | tags = tags_for("haskell", "8") 183 | args = { 184 | BASE_IMAGE = "haskell:8" 185 | } 186 | } 187 | 188 | target "haskell-810" { 189 | inherits = ["base"] 190 | tags = tags_for("haskell", "8.10") 191 | args = { 192 | BASE_IMAGE = "haskell:8.10" 193 | } 194 | } 195 | 196 | target "haskell-8102" { 197 | inherits = ["base"] 198 | tags = tags_for("haskell", "8.10.2") 199 | platforms = [ 200 | "linux/amd64" 201 | ] 202 | args = { 203 | BASE_IMAGE = "haskell:8.10.2" 204 | } 205 | } 206 | 207 | target "haskell-88" { 208 | inherits = ["base"] 209 | tags = tags_for("haskell", "8.8") 210 | platforms = [ 211 | "linux/amd64" 212 | ] 213 | args = { 214 | BASE_IMAGE = "haskell:8.8" 215 | } 216 | } 217 | 218 | target "haskell-86" { 219 | inherits = ["base"] 220 | tags = tags_for("haskell", "8.6") 221 | platforms = [ 222 | "linux/amd64" 223 | ] 224 | args = { 225 | BASE_IMAGE = "haskell:8.6" 226 | } 227 | } 228 | 229 | group "example" { 230 | targets = [ 231 | "example-brittany", 232 | "example-clang-format", 233 | "example-patchelf", 234 | ] 235 | } 236 | 237 | target "example-brittany" { 238 | dockerfile = "example/brittany/Dockerfile" 239 | contexts = { 240 | "magicpak/haskell:8.10.2-magicpak1.4.0" = "target:haskell-8102" 241 | } 242 | } 243 | 244 | target "example-clang-format" { 245 | dockerfile = "example/clang-format/Dockerfile" 246 | contexts = { 247 | "magicpak/debian:buster-magicpak1.4.0" = "target:debian-buster" 248 | } 249 | } 250 | 251 | target "example-patchelf" { 252 | dockerfile = "example/patchelf/Dockerfile" 253 | contexts = { 254 | "magicpak/cc:10-magicpak1.4.0" = "target:cc-10" 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /example/brittany/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM magicpak/haskell:8.10.2-magicpak1.4.0 2 | 3 | RUN apt-get -y update 4 | RUN apt-get -y install unzip libtinfo5 5 | 6 | ADD https://github.com/lspitzner/brittany/releases/download/0.13.1.0/brittany-0.13.1.0-linux.zip /tmp/brittany.zip 7 | RUN cd /tmp && unzip ./brittany.zip 8 | 9 | RUN magicpak /tmp/brittany /bundle -v \ 10 | --dynamic \ 11 | --dynamic-stdin "a = 1" \ 12 | --compress \ 13 | --upx-arg -9 \ 14 | --test \ 15 | --test-stdin "a= 1" \ 16 | --test-stdout "a = 1" \ 17 | --install-to /bin/ 18 | 19 | FROM scratch 20 | COPY --from=0 /bundle /. 21 | 22 | CMD ["/bin/brittany"] 23 | -------------------------------------------------------------------------------- /example/clang-format/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM magicpak/debian:buster-magicpak1.4.0 2 | 3 | RUN apt-get -y update 4 | RUN apt-get -y --no-install-recommends install clang-format 5 | 6 | RUN magicpak $(which clang-format) /bundle -v \ 7 | --compress \ 8 | --upx-arg --best \ 9 | --test \ 10 | --test-stdin "int main( ){ }" \ 11 | --test-stdout "int main() {}" \ 12 | --install-to /bin/ 13 | 14 | FROM scratch 15 | COPY --from=0 /bundle /. 16 | 17 | WORKDIR /workdir 18 | 19 | CMD ["/bin/clang-format"] 20 | -------------------------------------------------------------------------------- /example/patchelf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM magicpak/cc:10-magicpak1.4.0 2 | 3 | WORKDIR /usr/src/patchelf 4 | ADD https://github.com/NixOS/patchelf/archive/0.10.tar.gz patchelf.tar.gz 5 | RUN tar --strip-components=1 -xf patchelf.tar.gz 6 | 7 | RUN ./bootstrap.sh \ 8 | && ./configure \ 9 | && make \ 10 | && make install 11 | 12 | RUN magicpak $(which patchelf) /bundle -v \ 13 | --dynamic \ 14 | --dynamic-arg --help \ 15 | --compress \ 16 | --upx-arg -9 \ 17 | --test \ 18 | --test-command '/bin/patchelf --help' \ 19 | --install-to /bin/ 20 | 21 | FROM scratch 22 | COPY --from=0 /bundle /. 23 | 24 | WORKDIR /workdir 25 | 26 | CMD ["/bin/patchelf"] 27 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.65.0" 3 | components = [ "rustfmt", "clippy" ] 4 | targets = [ "x86_64-unknown-linux-musl" ] 5 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | pub mod bundle_dynamic_dependencies; 2 | pub mod bundle_executable; 3 | pub mod bundle_shared_object_dependencies; 4 | pub mod compress_executable; 5 | pub mod emit; 6 | pub mod exclude_glob; 7 | pub mod include_glob; 8 | pub mod make_directory; 9 | pub mod test; 10 | 11 | pub use bundle_dynamic_dependencies::*; 12 | pub use bundle_executable::*; 13 | pub use bundle_shared_object_dependencies::*; 14 | pub use compress_executable::*; 15 | pub use emit::*; 16 | pub use exclude_glob::*; 17 | pub use include_glob::*; 18 | pub use make_directory::*; 19 | pub use test::*; 20 | -------------------------------------------------------------------------------- /src/action/bundle_dynamic_dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::fmt::Debug; 4 | use std::io::Write; 5 | use std::path::PathBuf; 6 | use std::process::{Command, Stdio}; 7 | use std::rc::Rc; 8 | 9 | use crate::base::log::{log_output, CommandLogExt}; 10 | use crate::base::trace::{ChildTraceExt, CommandTraceExt, SyscallHandler}; 11 | use crate::base::{Error, Result}; 12 | use crate::domain::{Bundle, Executable}; 13 | 14 | pub fn bundle_dynamic_dependencies( 15 | bundle: &mut Bundle, 16 | exe: &Executable, 17 | args: I, 18 | stdin: Option, 19 | ) -> Result<()> 20 | where 21 | I: IntoIterator + Debug, 22 | S: AsRef, 23 | T: AsRef, 24 | { 25 | tracing::info!( 26 | exe = %exe.path().display(), 27 | args = ?args, 28 | stdin = ?stdin.as_ref().map(AsRef::as_ref), 29 | "action: bundle dynamically analyzed dependencies", 30 | ); 31 | 32 | let mut child = Command::new(exe.path()) 33 | .args(args) 34 | .stdin(Stdio::piped()) 35 | .stdout(Stdio::piped()) 36 | .stderr(Stdio::piped()) 37 | .traceme() 38 | .spawn_with_log()?; 39 | 40 | if let Some(content) = stdin { 41 | // unwrap is ok here because stdin is surely piped 42 | write!(child.stdin.take().unwrap(), "{}", content.as_ref())?; 43 | } 44 | 45 | let bundle_ref = Rc::new(RefCell::new(bundle)); 46 | 47 | let output = child.trace_syscalls(SyscallHandler { 48 | open: |pathname, _| open_handler(&bundle_ref, "open", pathname), 49 | openat: |_, pathname, _| open_handler(&bundle_ref, "openat", pathname), 50 | })?; 51 | log_output("", &output); 52 | 53 | if !output.status.success() { 54 | return Err(Error::DynamicFailed(output.status)); 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | fn open_handler(bundle: &Rc>, name: &str, pathname: OsString) { 61 | let path: PathBuf = pathname.into(); 62 | 63 | tracing::debug!( 64 | syscall = %name, 65 | open_path = %path.display(), 66 | "action: bundle_dynamic_dependencies: open syscall", 67 | ); 68 | 69 | if path.is_file() { 70 | tracing::info!( 71 | path = %path.display(), 72 | "action: bundle_dynamic_dependencies: found path", 73 | ); 74 | 75 | bundle.borrow_mut().add(path); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/action/bundle_executable.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use crate::base::Result; 4 | use crate::domain::{Bundle, BundlePath, Executable}; 5 | 6 | pub fn bundle_executable( 7 | bundle: &mut Bundle, 8 | exe: &Executable, 9 | input_path: P, 10 | install_path: Option, 11 | ) -> Result<()> 12 | where 13 | S: AsRef, 14 | P: AsRef, 15 | { 16 | tracing::info!( 17 | exe = %exe.path().display(), 18 | install_path = ?install_path.as_ref().map(|x| x.as_ref()), 19 | "action: bundle executable", 20 | ); 21 | 22 | match install_path { 23 | Some(p) => { 24 | let mut path = p.as_ref().to_owned(); 25 | 26 | if path.ends_with('/') { 27 | path.push_str(exe.name()); 28 | tracing::info!( 29 | completed_path = %path, 30 | "action: bundle_executable: completing full path", 31 | ); 32 | } 33 | bundle.add_file_from(BundlePath::projection(&path), exe.path()); 34 | } 35 | None => bundle.add_file_from(BundlePath::projection(&input_path), exe.path()), 36 | } 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/action/bundle_shared_object_dependencies.rs: -------------------------------------------------------------------------------- 1 | use crate::base::{Error, Result}; 2 | use crate::domain::{Bundle, Executable}; 3 | 4 | fn bundle_shared_object_dependencies_impl( 5 | bundle: &mut Bundle, 6 | exe: &Executable, 7 | cc: &str, 8 | noload_resolver: bool, 9 | ) -> Result<()> { 10 | tracing::info!( 11 | exe = %exe.path().display(), 12 | "action: bundle shared object dependencies", 13 | ); 14 | 15 | let cc_path = which::which(cc).map_err(|e| Error::ExecutableLocateFailed(cc.to_owned(), e))?; 16 | 17 | bundle.add(exe.interpreter()); 18 | if noload_resolver { 19 | bundle.add(exe.dynamic_libraries_noload(cc_path)?); 20 | } else { 21 | bundle.add(exe.dynamic_libraries(cc_path)?); 22 | } 23 | 24 | Ok(()) 25 | } 26 | 27 | pub fn bundle_shared_object_dependencies( 28 | bundle: &mut Bundle, 29 | exe: &Executable, 30 | cc: &str, 31 | ) -> Result<()> { 32 | bundle_shared_object_dependencies_impl(bundle, exe, cc, false) 33 | } 34 | 35 | pub fn bundle_shared_object_dependencies_noload( 36 | bundle: &mut Bundle, 37 | exe: &Executable, 38 | cc: &str, 39 | ) -> Result<()> { 40 | bundle_shared_object_dependencies_impl(bundle, exe, cc, true) 41 | } 42 | -------------------------------------------------------------------------------- /src/action/compress_executable.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | use crate::base::{Error, Result}; 4 | use crate::domain::Executable; 5 | 6 | pub fn compress_exexcutable(exe: &mut Executable, upx: &str, upx_opts: I) -> Result<()> 7 | where 8 | I: IntoIterator, 9 | S: AsRef, 10 | { 11 | tracing::info!(exe = %exe.path().display(), "action: compress executable"); 12 | 13 | let upx_path = 14 | which::which(upx).map_err(|e| Error::ExecutableLocateFailed(upx.to_owned(), e))?; 15 | 16 | let compressed = exe.compressed(upx_path, upx_opts)?; 17 | tracing::debug!( 18 | path = %compressed.path().display(), 19 | "action: compress_exexcutable: compressed executable", 20 | ); 21 | 22 | *exe = compressed; 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /src/action/emit.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | use crate::base::{Error, Result}; 5 | use crate::domain::Bundle; 6 | 7 | pub fn emit

(bundle: &mut Bundle, path: P) -> Result<()> 8 | where 9 | P: AsRef, 10 | { 11 | let dest = path.as_ref(); 12 | tracing::info!(dest = %dest.display(), "action: emit"); 13 | 14 | if dest.exists() { 15 | if !dest.is_dir() { 16 | return Err(Error::InvalidDestination(dest.to_owned())); 17 | } 18 | if dest.read_dir()?.next().is_some() { 19 | return Err(Error::NonEmptyDestionation(dest.to_owned())); 20 | } 21 | } else { 22 | tracing::info!( 23 | dest = %dest.display(), 24 | "action: emit: creating destination dir as it does not exist", 25 | ); 26 | fs::create_dir(dest)?; 27 | }; 28 | bundle.emit(dest.canonicalize()?) 29 | } 30 | -------------------------------------------------------------------------------- /src/action/exclude_glob.rs: -------------------------------------------------------------------------------- 1 | use crate::base::Result; 2 | use crate::domain::Bundle; 3 | 4 | pub fn exclude_glob(bundle: &mut Bundle, pattern: &str) -> Result<()> { 5 | tracing::info!(%pattern, "action: exclude using glob"); 6 | 7 | let pattern = glob::Pattern::new(pattern)?; 8 | bundle.filter(|path| { 9 | let str_path = path.to_str_lossy(); 10 | let pseudo_path = format!("/{}", str_path); 11 | tracing::debug!( 12 | %pattern, 13 | %pseudo_path, 14 | "action: exclude_glob: matching with pseudo path", 15 | ); 16 | !pattern.matches(&pseudo_path) 17 | }); 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /src/action/include_glob.rs: -------------------------------------------------------------------------------- 1 | use crate::base::{Error, Result}; 2 | use crate::domain::{Bundle, Executable}; 3 | 4 | fn include_glob_impl( 5 | bundle: &mut Bundle, 6 | pattern: &str, 7 | cc: &str, 8 | noload_resolver: bool, 9 | ) -> Result<()> { 10 | tracing::info!(%pattern, "action: include using glob"); 11 | 12 | let cc_path = which::which(cc).map_err(|e| Error::ExecutableLocateFailed(cc.to_owned(), e))?; 13 | 14 | for entry in glob::glob(pattern)? { 15 | match entry { 16 | Ok(path) => { 17 | if let Ok(obj) = Executable::load(&path) { 18 | bundle.add(obj.interpreter()); 19 | if noload_resolver { 20 | bundle.add(obj.dynamic_libraries_noload(&cc_path)?); 21 | } else { 22 | bundle.add(obj.dynamic_libraries(&cc_path)?); 23 | } 24 | } 25 | bundle.add(path); 26 | } 27 | Err(e) => tracing::warn!(error = %e, "action: include_glob: Ignoring glob match"), 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | 34 | pub fn include_glob(bundle: &mut Bundle, pattern: &str, cc: &str) -> Result<()> { 35 | include_glob_impl(bundle, pattern, cc, false) 36 | } 37 | 38 | pub fn include_glob_noload(bundle: &mut Bundle, pattern: &str, cc: &str) -> Result<()> { 39 | include_glob_impl(bundle, pattern, cc, true) 40 | } 41 | -------------------------------------------------------------------------------- /src/action/make_directory.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::{Bundle, BundlePath}; 2 | 3 | pub fn make_directory(bundle: &mut Bundle, path: &str) { 4 | tracing::info!(%path, "action: make directory"); 5 | bundle.mkdir(BundlePath::projection(&path)); 6 | } 7 | -------------------------------------------------------------------------------- /src/action/test.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::process::{Command, Stdio}; 3 | 4 | use crate::base::log::{ChildLogExt, CommandLogExt}; 5 | use crate::base::{Error, Result}; 6 | use crate::domain::jail::CommandJailExt; 7 | use crate::domain::{Bundle, Executable}; 8 | 9 | pub fn test( 10 | bundle: &Bundle, 11 | exe: &Executable, 12 | command: Option, 13 | command_stdin: Option, 14 | command_stdout: Option, 15 | busybox: &str, 16 | ) -> Result<()> 17 | where 18 | S: AsRef, 19 | T: AsRef, 20 | U: AsRef, 21 | { 22 | let command = command 23 | .as_ref() 24 | .map(AsRef::as_ref) 25 | .unwrap_or_else(|| exe.name()); 26 | 27 | tracing::info!(%command, "action: test the bundle"); 28 | 29 | let busybox_path = 30 | which::which(busybox).map_err(|e| Error::ExecutableLocateFailed(busybox.to_owned(), e))?; 31 | 32 | let mut test_bundle = bundle.clone(); 33 | test_bundle.add_pseudo_proc(exe); 34 | 35 | let jail = test_bundle.create_jail()?; 36 | jail.install_busybox(busybox_path)?; 37 | 38 | let mut child = Command::new("/bin/sh") 39 | .arg("-c") 40 | .arg(command) 41 | .stdin(Stdio::piped()) 42 | .stdout(Stdio::piped()) 43 | .stderr(Stdio::piped()) 44 | .in_jail(&jail) 45 | .spawn_with_log()?; 46 | 47 | if let Some(content) = command_stdin { 48 | // unwrap is ok here because stdin is surely piped 49 | write!(child.stdin.as_mut().unwrap(), "{}", content.as_ref())?; 50 | } 51 | 52 | let output = child.wait_output_with_log()?; 53 | 54 | if !output.status.success() { 55 | return Err(Error::TestFailed(command.to_owned())); 56 | } 57 | tracing::info!(status = %output.status, "action: test command succeeded"); 58 | 59 | if let Some(content) = command_stdout { 60 | let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); 61 | if stdout != content.as_ref() { 62 | return Err(Error::TestStdoutMismatch { 63 | expected: content.as_ref().to_string(), 64 | got: stdout, 65 | }); 66 | } 67 | } 68 | 69 | tracing::info!("action: test succeeded"); 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /src/base.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod log; 3 | pub mod trace; 4 | 5 | pub use error::{Error, Result}; 6 | -------------------------------------------------------------------------------- /src/base/error.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::path::PathBuf; 3 | use std::process::ExitStatus; 4 | use std::{error, fmt, io, result, str}; 5 | 6 | use goblin::error as goblin; 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | InvalidDestination(PathBuf), 11 | NonEmptyDestionation(PathBuf), 12 | InvalidGlobPattern(String), 13 | SharedLibraryLookup(String), 14 | ResolverCompilation(String), 15 | MalformedExecutable(String), 16 | ValueNotFoundInStrtab { tag: u64, val: u64 }, 17 | InterpretorNotFound, 18 | BusyBoxInstall(String), 19 | TestFailed(String), 20 | TestStdoutMismatch { expected: String, got: String }, 21 | ExecutableLocateFailed(String, which::Error), 22 | Upx(String), 23 | DynamicFailed(ExitStatus), 24 | Encoding(str::Utf8Error), 25 | PathEncoding(OsString), 26 | InvalidObjectPath(PathBuf), 27 | DynamicWithMultipleInputsUnsupported, 28 | TestWithMultipleInputsUnsupported, 29 | IO(io::Error), 30 | } 31 | 32 | pub type Result = result::Result; 33 | 34 | impl fmt::Display for Error { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | match self { 37 | Error::InvalidDestination(path) => { 38 | write!(f, "The destination is invalid: {}", path.display()) 39 | } 40 | Error::NonEmptyDestionation(path) => { 41 | write!(f, "The destination is not empty: {}", path.display()) 42 | } 43 | Error::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e), 44 | Error::SharedLibraryLookup(e) => write!(f, "Unable to lookup shared library: {}", e), 45 | Error::ResolverCompilation(e) => write!( 46 | f, 47 | "Error happend during the compilation of library resolver: {}", 48 | e 49 | ), 50 | Error::MalformedExecutable(e) => write!(f, "The executable is malformed: {}", e), 51 | Error::ValueNotFoundInStrtab { tag, val } => write!( 52 | f, 53 | "The executable is malformed: Value {} with tag {} is not found on strtab", 54 | val, tag 55 | ), 56 | Error::InterpretorNotFound => { 57 | write!(f, "Could not find an interpreter for the executable") 58 | } 59 | Error::BusyBoxInstall(e) => write!( 60 | f, 61 | "Unable to install busybox to the temporary directory: {}", 62 | e 63 | ), 64 | Error::TestFailed(cmd) => write!(f, "Test failed: {} returned non-zero exit code", cmd), 65 | Error::TestStdoutMismatch { expected, got } => write!( 66 | f, 67 | "Test failed: Test command stdout mismatch. expected: '{}', but got '{}'", 68 | expected, got 69 | ), 70 | Error::Encoding(e) => write!(f, "Encoding error: {}", e), 71 | Error::ExecutableLocateFailed(exe, e) => { 72 | write!(f, "Unable to locate executable '{}': {}", exe, e) 73 | } 74 | Error::Upx(e) => write!(f, "upx failed with non-zero exit code: {}", e), 75 | Error::DynamicFailed(status) => { 76 | write!(f, "Dynamic analysis subproecss failed: {}", status) 77 | } 78 | Error::PathEncoding(p) => write!( 79 | f, 80 | "Unable to interpret the path as UTF-8: {}", 81 | p.to_string_lossy() 82 | ), 83 | Error::InvalidObjectPath(p) => { 84 | write!(f, "Invalid ELF object file path '{}'", p.display()) 85 | } 86 | Error::TestWithMultipleInputsUnsupported => { 87 | write!(f, "use of --test with multiple inputs is not supported") 88 | } 89 | Error::DynamicWithMultipleInputsUnsupported => { 90 | write!(f, "use of --dynamic with multiple inputs is not supported") 91 | } 92 | Error::IO(e) => write!(f, "IO error: {}", e), 93 | } 94 | } 95 | } 96 | 97 | impl error::Error for Error {} 98 | 99 | impl From for Error { 100 | fn from(err: io::Error) -> Self { 101 | Error::IO(err) 102 | } 103 | } 104 | 105 | impl From for Error { 106 | fn from(err: str::Utf8Error) -> Self { 107 | Error::Encoding(err) 108 | } 109 | } 110 | 111 | impl From for Error { 112 | fn from(err: goblin::Error) -> Self { 113 | match err { 114 | goblin::Error::IO(e) => Error::IO(e), 115 | e => Error::MalformedExecutable(e.to_string()), 116 | } 117 | } 118 | } 119 | 120 | impl From for Error { 121 | fn from(err: glob::PatternError) -> Self { 122 | Error::InvalidGlobPattern(err.msg.to_string()) 123 | } 124 | } 125 | 126 | impl From for Error { 127 | fn from(err: nix::Error) -> Self { 128 | Error::IO(err.into()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/base/log.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::process::{Child, Command, ExitStatus, Output}; 3 | 4 | pub trait CommandLogExt { 5 | fn spawn_with_log(&mut self) -> Result; 6 | 7 | fn output_with_log(&mut self) -> Result; 8 | 9 | fn status_with_log(&mut self) -> Result { 10 | Ok(self.output_with_log()?.status) 11 | } 12 | } 13 | 14 | pub trait ChildLogExt { 15 | fn wait_output_with_log(self) -> Result; 16 | } 17 | 18 | impl CommandLogExt for Command { 19 | fn spawn_with_log(&mut self) -> Result { 20 | tracing::debug!(command = ?self, "spawn"); 21 | self.spawn() 22 | } 23 | 24 | fn output_with_log(&mut self) -> Result { 25 | let output = self.output()?; 26 | let command_line = format!("{:?}", self); 27 | log_output(&command_line, &output); 28 | Ok(output) 29 | } 30 | } 31 | 32 | impl ChildLogExt for Child { 33 | fn wait_output_with_log(self) -> Result { 34 | let output = self.wait_with_output()?; 35 | log_output("", &output); 36 | Ok(output) 37 | } 38 | } 39 | 40 | pub fn log_output(command_line: &str, output: &Output) { 41 | let mut message = format!("command: {}\n => {}", command_line, output.status); 42 | 43 | if !output.stdout.is_empty() { 44 | let stdout = format_lines(String::from_utf8_lossy(&output.stdout)); 45 | message.push_str("\n => stdout: "); 46 | message.push_str(&stdout); 47 | } 48 | if !output.stderr.is_empty() { 49 | let stderr = format_lines(String::from_utf8_lossy(&output.stderr)); 50 | message.push_str("\n => stderr: "); 51 | message.push_str(&stderr); 52 | } 53 | 54 | tracing::debug!("{}", message); 55 | } 56 | 57 | fn format_lines(s: S) -> String 58 | where 59 | S: AsRef, 60 | { 61 | let string = s.as_ref().trim(); 62 | if string.lines().nth(1).is_some() { 63 | string 64 | .lines() 65 | .fold(String::new(), |acc, line| format!("{}\n | {}", acc, line)) 66 | } else { 67 | string.to_owned() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/base/trace.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::io::Read; 3 | use std::os::unix::ffi::OsStringExt; 4 | use std::os::unix::process::{CommandExt, ExitStatusExt}; 5 | use std::process::{Child, Command, ExitStatus, Output}; 6 | 7 | use crate::base::Result; 8 | 9 | use nix::libc; 10 | 11 | pub trait CommandTraceExt { 12 | fn traceme(&mut self) -> &mut Command; 13 | } 14 | 15 | impl CommandTraceExt for Command { 16 | fn traceme(&mut self) -> &mut Command { 17 | unsafe { self.pre_exec(|| nix::sys::ptrace::traceme().map_err(Into::into)) } 18 | } 19 | } 20 | 21 | pub struct SyscallHandler { 22 | pub open: FOpen, 23 | pub openat: FOpenAt, 24 | } 25 | 26 | pub trait ChildTraceExt { 27 | fn trace_syscalls( 28 | self, 29 | handler: SyscallHandler, 30 | ) -> Result 31 | where 32 | FOpen: FnMut(OsString, i32), 33 | FOpenAt: FnMut(i32, OsString, i32); 34 | } 35 | 36 | impl ChildTraceExt for Child { 37 | fn trace_syscalls( 38 | mut self, 39 | mut handler: SyscallHandler, 40 | ) -> Result 41 | where 42 | FOpen: FnMut(OsString, i32), 43 | FOpenAt: FnMut(i32, OsString, i32), 44 | { 45 | use nix::sys::signal::Signal; 46 | use nix::sys::wait::WaitStatus; 47 | 48 | let child_pid = nix::unistd::Pid::from_raw(self.id() as i32); 49 | 50 | let wstatus = waitpid(child_pid)?; 51 | match nix::sys::wait::WaitStatus::from_raw(child_pid, wstatus)? { 52 | WaitStatus::Stopped(_, Signal::SIGTRAP) => (), 53 | WaitStatus::Signaled { .. } 54 | | WaitStatus::Stopped { .. } 55 | | WaitStatus::Exited { .. } => { 56 | let status = ExitStatus::from_raw(wstatus); 57 | return output_of_child(&mut self, status); 58 | } 59 | _ => unreachable!(), 60 | } 61 | 62 | // TODO: should we handle forks? 63 | use nix::sys::ptrace::Options; 64 | nix::sys::ptrace::setoptions( 65 | child_pid, 66 | Options::PTRACE_O_TRACESYSGOOD | Options::PTRACE_O_EXITKILL, 67 | )?; 68 | nix::sys::ptrace::syscall(child_pid, None)?; 69 | 70 | loop { 71 | let wstatus = waitpid(child_pid)?; 72 | match nix::sys::wait::WaitStatus::from_raw(child_pid, wstatus)? { 73 | WaitStatus::Signaled { .. } | WaitStatus::Exited { .. } => { 74 | let status = ExitStatus::from_raw(wstatus); 75 | return output_of_child(&mut self, status); 76 | } 77 | WaitStatus::Stopped(pid, sig) => { 78 | tracing::warn!( 79 | signal = %sig, 80 | "trace_syscalls: stopped by signal, we attempt to continue", 81 | ); 82 | nix::sys::ptrace::syscall(pid, None)?; 83 | } 84 | WaitStatus::PtraceSyscall(pid) => { 85 | handle_syscall(&mut handler, pid)?; 86 | nix::sys::ptrace::syscall(pid, None)?; 87 | } 88 | _ => unreachable!(), 89 | } 90 | } 91 | } 92 | } 93 | 94 | fn output_of_child(child: &mut Child, status: ExitStatus) -> Result { 95 | let mut stdout = Vec::new(); 96 | let mut stderr = Vec::new(); 97 | if let Some(mut child_stdout) = child.stdout.take() { 98 | child_stdout.read_to_end(&mut stdout)?; 99 | } 100 | if let Some(mut child_stderr) = child.stderr.take() { 101 | child_stderr.read_to_end(&mut stderr)?; 102 | } 103 | let output = Output { 104 | status, 105 | stdout, 106 | stderr, 107 | }; 108 | Ok(output) 109 | } 110 | 111 | #[cfg(target_arch = "x86_64")] 112 | fn handle_syscall( 113 | handler: &mut SyscallHandler, 114 | pid: nix::unistd::Pid, 115 | ) -> Result<()> 116 | where 117 | FOpen: FnMut(OsString, i32), 118 | FOpenAt: FnMut(i32, OsString, i32), 119 | { 120 | let regs = getregs(pid)?; 121 | match regs.orig_rax as i64 { 122 | libc::SYS_openat => { 123 | let dirfd = regs.rdi as i32; 124 | let pathname = read_string_at(pid, regs.rsi)?; 125 | let flags = regs.rdx as i32; 126 | (handler.openat)(dirfd, pathname, flags); 127 | } 128 | libc::SYS_open => { 129 | let pathname = read_string_at(pid, regs.rdi)?; 130 | let flags = regs.rsi as i32; 131 | (handler.open)(pathname, flags); 132 | } 133 | _ => (), 134 | } 135 | Ok(()) 136 | } 137 | 138 | #[cfg(target_arch = "aarch64")] 139 | fn handle_syscall( 140 | handler: &mut SyscallHandler, 141 | pid: nix::unistd::Pid, 142 | ) -> Result<()> 143 | where 144 | FOpen: FnMut(OsString, i32), 145 | FOpenAt: FnMut(i32, OsString, i32), 146 | { 147 | let regs = getregs(pid)?; 148 | match regs.regs[8] as i64 { 149 | libc::SYS_openat => { 150 | let dirfd = regs.regs[0] as i32; 151 | let pathname = read_string_at(pid, regs.regs[1])?; 152 | let flags = regs.regs[2] as i32; 153 | (handler.openat)(dirfd, pathname, flags); 154 | } 155 | _ => (), 156 | } 157 | Ok(()) 158 | } 159 | 160 | // libc::NT_PRSTATUS unavailable on musl 161 | const NT_PRSTATUS: libc::c_int = 1; 162 | 163 | fn getregs(pid: nix::unistd::Pid) -> Result { 164 | let mut data = std::mem::MaybeUninit::::uninit(); 165 | let mut iov = libc::iovec { 166 | iov_base: data.as_mut_ptr() as *mut std::ffi::c_void, 167 | iov_len: std::mem::size_of::(), 168 | }; 169 | 170 | let res = unsafe { libc::ptrace(libc::PTRACE_GETREGSET, pid, NT_PRSTATUS, &mut iov as *mut _) }; 171 | 172 | nix::errno::Errno::result(res)?; 173 | Ok(unsafe { data.assume_init() }) 174 | } 175 | 176 | fn read_string_at(pid: nix::unistd::Pid, mut addr: u64) -> Result { 177 | use std::ffi::c_void; 178 | 179 | let mut result = Vec::new(); 180 | loop { 181 | let word = nix::sys::ptrace::read(pid, addr as *mut c_void)? as u32; 182 | let bytes: [u8; 4] = word.to_ne_bytes(); 183 | for byte in bytes.iter() { 184 | if *byte == 0 { 185 | return Ok(OsString::from_vec(result)); 186 | } 187 | result.push(*byte); 188 | } 189 | addr += 4; 190 | } 191 | } 192 | 193 | // we need a raw wstatus but nix::sys::wait::waitpid does not expose it 194 | fn waitpid(pid: nix::unistd::Pid) -> nix::Result { 195 | let mut status: i32 = 0; 196 | 197 | let res = unsafe { nix::libc::waitpid(pid.into(), &mut status as *mut nix::libc::c_int, 0) }; 198 | 199 | nix::errno::Errno::result(res)?; 200 | Ok(status) 201 | } 202 | 203 | #[cfg(test)] 204 | mod tests { 205 | use super::*; 206 | use assert_cmd::prelude::*; 207 | use assert_fs::prelude::*; 208 | use std::cell::RefCell; 209 | use std::rc::Rc; 210 | 211 | #[test] 212 | fn test_trace() -> std::result::Result<(), Box> { 213 | let test_path = assert_fs::NamedTempFile::new("test")?; 214 | test_path.touch()?; 215 | let child = Command::new("cat") 216 | .arg(test_path.path()) 217 | .traceme() 218 | .spawn()?; 219 | 220 | let paths = Rc::new(RefCell::new(Vec::new())); 221 | child 222 | .trace_syscalls(SyscallHandler { 223 | open: |pathname, _| paths.borrow_mut().push(pathname), 224 | openat: |_, pathname, _| paths.borrow_mut().push(pathname), 225 | })? 226 | .assert() 227 | .success(); 228 | 229 | assert_eq!( 230 | true, 231 | Rc::try_unwrap(paths) 232 | .unwrap() 233 | .into_inner() 234 | .iter() 235 | .any(|p| p == test_path.path()) 236 | ); 237 | Ok(()) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use magicpak::action; 4 | use magicpak::base::{Error, Result}; 5 | use magicpak::domain::{Bundle, Executable}; 6 | 7 | use clap::Parser; 8 | 9 | #[derive(Debug, Clone, Copy, clap::ValueEnum)] 10 | #[value(rename_all = "PascalCase")] 11 | enum LogLevel { 12 | Off, 13 | Error, 14 | Warn, 15 | Info, 16 | Debug, 17 | } 18 | 19 | impl LogLevel { 20 | fn to_level_filter(self) -> tracing_subscriber::filter::LevelFilter { 21 | use tracing_subscriber::filter::LevelFilter; 22 | match self { 23 | LogLevel::Off => LevelFilter::OFF, 24 | LogLevel::Error => LevelFilter::ERROR, 25 | LogLevel::Warn => LevelFilter::WARN, 26 | LogLevel::Info => LevelFilter::INFO, 27 | LogLevel::Debug => LevelFilter::DEBUG, 28 | } 29 | } 30 | } 31 | 32 | #[derive(Parser)] 33 | #[command(name = "magicpak")] 34 | struct Args { 35 | #[arg(value_name = "INPUT", required = true)] 36 | /// Input executable 37 | input: Vec, 38 | 39 | #[arg(value_name = "OUTPUT")] 40 | /// Output destination 41 | output: PathBuf, 42 | 43 | #[arg(short, long, value_name = "GLOB")] 44 | /// Additionally include files/directories with glob patterns 45 | include: Vec, 46 | 47 | #[arg(short, long, value_name = "GLOB")] 48 | /// Exclude files/directories from the resulting bundle with glob patterns 49 | exclude: Vec, 50 | 51 | #[arg(long, value_name = "PATH")] 52 | /// Make directories in the resulting bundle 53 | mkdir: Vec, 54 | 55 | #[arg(short = 'r', long, value_name = "PATH")] 56 | /// Specify the installation path of the executable in the bundle 57 | install_to: Option, 58 | 59 | #[arg(long, value_name = "LEVEL", default_value = "Warn")] 60 | /// Specify the log level 61 | log_level: LogLevel, 62 | 63 | #[arg(short, long)] 64 | /// Verbose mode, same as --log-level Info 65 | verbose: bool, 66 | 67 | #[arg(short, long)] 68 | /// Enable testing 69 | test: bool, 70 | 71 | #[arg(long, value_name = "COMMAND")] 72 | /// Specify the test command to use in --test 73 | test_command: Option, 74 | 75 | #[arg(long, value_name = "CONTENT")] 76 | /// Specify stdin content supplied to the test command in --test 77 | test_stdin: Option, 78 | 79 | #[arg(long, value_name = "CONTENT")] 80 | /// Test stdout of the test command 81 | test_stdout: Option, 82 | 83 | #[arg(short, long)] 84 | /// Enable dynamic analysis 85 | dynamic: bool, 86 | 87 | #[arg( 88 | long, 89 | value_name = "ARG", 90 | allow_hyphen_values = true, 91 | number_of_values = 1 92 | )] 93 | /// Specify arguments passed to the executable in --dynamic 94 | dynamic_arg: Vec, 95 | 96 | #[arg(long, value_name = "CONTENT")] 97 | /// Specify stdin content supplied to the executable in --dynamic 98 | dynamic_stdin: Option, 99 | 100 | #[arg(short, long)] 101 | /// Compress the executable with npx 102 | compress: bool, 103 | 104 | #[arg( 105 | long, 106 | value_name = "ARG", 107 | allow_hyphen_values = true, 108 | number_of_values = 1 109 | )] 110 | /// Specify arguments passed to upx in --compress 111 | upx_arg: Vec, 112 | 113 | #[arg(long, value_name = "PATH or NAME", default_value = "busybox")] 114 | /// Specify the path or name of busybox that would be used in testing 115 | busybox: String, 116 | 117 | #[arg(long, value_name = "PATH or NAME", default_value = "upx")] 118 | /// Specify the path or name of upx that would be used in compression 119 | upx: String, 120 | 121 | #[arg(long, value_name = "PATH or NAME", default_value = "cc", env = "CC")] 122 | /// Specify the path or name of c compiler that would be used in 123 | /// the name resolution of shared library dependencies 124 | cc: String, 125 | 126 | #[clap(long)] 127 | /// [EXPERIMENTAL] Resolve dynamic library paths without loading in dlopen(3). 128 | experimental_noload_resolver: bool, 129 | } 130 | 131 | fn run(args: &Args) -> Result<()> { 132 | let mut bundle = Bundle::new(); 133 | let mut exes = args 134 | .input 135 | .iter() 136 | .map(Executable::load) 137 | .collect::>>()?; 138 | 139 | for exe in &exes { 140 | if args.experimental_noload_resolver { 141 | action::bundle_shared_object_dependencies_noload(&mut bundle, exe, &args.cc)?; 142 | } else { 143 | action::bundle_shared_object_dependencies(&mut bundle, exe, &args.cc)?; 144 | } 145 | } 146 | 147 | if args.dynamic { 148 | let &[ref exe] = &exes[..] else { 149 | return Err(Error::DynamicWithMultipleInputsUnsupported); 150 | }; 151 | 152 | action::bundle_dynamic_dependencies( 153 | &mut bundle, 154 | exe, 155 | &args.dynamic_arg, 156 | args.dynamic_stdin.as_ref(), 157 | )?; 158 | } 159 | 160 | if args.compress { 161 | for exe in &mut exes { 162 | action::compress_exexcutable(exe, &args.upx, &args.upx_arg)?; 163 | } 164 | } 165 | 166 | for (exe, input) in exes.iter().zip(&args.input) { 167 | action::bundle_executable(&mut bundle, exe, input, args.install_to.as_ref())?; 168 | } 169 | 170 | for dir in &args.mkdir { 171 | action::make_directory(&mut bundle, dir); 172 | } 173 | 174 | for glob in &args.include { 175 | if args.experimental_noload_resolver { 176 | action::include_glob_noload(&mut bundle, glob, &args.cc)?; 177 | } else { 178 | action::include_glob(&mut bundle, glob, &args.cc)?; 179 | } 180 | } 181 | 182 | for glob in &args.exclude { 183 | action::exclude_glob(&mut bundle, glob)?; 184 | } 185 | 186 | if args.test { 187 | let &[ref exe] = &exes[..] else { 188 | return Err(Error::TestWithMultipleInputsUnsupported); 189 | }; 190 | 191 | action::test( 192 | &bundle, 193 | exe, 194 | args.test_command.as_ref(), 195 | args.test_stdin.as_ref(), 196 | args.test_stdout.as_ref(), 197 | &args.busybox, 198 | )?; 199 | } 200 | 201 | action::emit(&mut bundle, &args.output)?; 202 | 203 | Ok(()) 204 | } 205 | 206 | fn main() { 207 | let args = Args::parse(); 208 | 209 | let level_filter = if args.verbose { 210 | tracing_subscriber::filter::LevelFilter::INFO 211 | } else { 212 | args.log_level.to_level_filter() 213 | }; 214 | 215 | use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; 216 | tracing_subscriber::registry() 217 | .with(level_filter) 218 | .with( 219 | tracing_subscriber::fmt::layer() 220 | .with_writer(std::io::stderr) 221 | .with_target(false) 222 | .without_time(), 223 | ) 224 | .init(); 225 | 226 | std::process::exit(match run(&args) { 227 | Ok(()) => 0, 228 | Err(e) => { 229 | eprintln!("error: {}", e); 230 | 1 231 | } 232 | }); 233 | } 234 | -------------------------------------------------------------------------------- /src/domain.rs: -------------------------------------------------------------------------------- 1 | pub mod bundle; 2 | pub mod bundle_path; 3 | pub mod executable; 4 | pub mod jail; 5 | pub mod resource; 6 | 7 | pub use bundle::Bundle; 8 | pub use bundle_path::{BundlePath, BundlePathBuf}; 9 | pub use executable::Executable; 10 | pub use jail::Jail; 11 | pub use resource::Resource; 12 | -------------------------------------------------------------------------------- /src/domain/bundle.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::default::Default; 3 | use std::fs; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use crate::base::Result; 7 | use crate::domain::{BundlePath, BundlePathBuf, Executable, Jail, Resource}; 8 | 9 | #[derive(Clone)] 10 | enum Source { 11 | NewDirectory, 12 | NewFile(Vec), 13 | CopyFrom(PathBuf), 14 | } 15 | 16 | #[derive(Default, Clone)] 17 | pub struct Bundle { 18 | entries: HashMap, 19 | } 20 | 21 | impl Bundle { 22 | pub fn new() -> Bundle { 23 | Bundle { 24 | entries: HashMap::new(), 25 | } 26 | } 27 | 28 | pub fn mkdir

(&mut self, path: P) 29 | where 30 | P: AsRef, 31 | { 32 | tracing::debug!(path = %path.as_ref().display(), "bundle: mkdir"); 33 | self.entries 34 | .insert(path.as_ref().to_owned(), Source::NewDirectory); 35 | } 36 | 37 | pub fn add_file

(&mut self, path: P, content: Vec) 38 | where 39 | P: AsRef, 40 | { 41 | tracing::debug!( 42 | path = %path.as_ref().display(), 43 | "bundle: add_file", 44 | ); 45 | self.entries 46 | .insert(path.as_ref().to_owned(), Source::NewFile(content)); 47 | } 48 | 49 | pub fn add_file_from(&mut self, path: P, from: Q) 50 | where 51 | P: AsRef, 52 | Q: AsRef, 53 | { 54 | debug_assert!(from.as_ref().is_absolute()); 55 | tracing::debug!( 56 | from = %from.as_ref().display(), 57 | path = %path.as_ref().display(), 58 | "bundle: copy", 59 | ); 60 | 61 | self.entries.insert( 62 | path.as_ref().to_owned(), 63 | Source::CopyFrom(from.as_ref().to_owned()), 64 | ); 65 | } 66 | 67 | pub fn add(&mut self, resource: R) 68 | where 69 | R: Resource, 70 | { 71 | resource.bundle_to(self); 72 | } 73 | 74 | pub fn filter

(&mut self, mut predicate: P) 75 | where 76 | P: FnMut(&BundlePathBuf) -> bool, 77 | { 78 | let entries = std::mem::take(&mut self.entries); 79 | let updated = entries.into_iter().filter(|(k, _)| predicate(k)).collect(); 80 | self.entries = updated; 81 | } 82 | 83 | pub fn emit

(&self, dest: P) -> Result<()> 84 | where 85 | P: AsRef, 86 | { 87 | for (bpath, source) in self.entries.iter() { 88 | match source { 89 | Source::NewDirectory => { 90 | let path = bpath.reify(&dest); 91 | tracing::info!(path = %path.display(), "emit: mkdir"); 92 | fs::create_dir_all(path)? 93 | } 94 | Source::NewFile(blob) => { 95 | let path = bpath.reify(&dest); 96 | tracing::info!(path = %path.display(), "emit: write"); 97 | create_parent_dir(&path)?; 98 | fs::write(path, blob)? 99 | } 100 | Source::CopyFrom(src_path) => { 101 | sync_copy(src_path, bpath, dest.as_ref())?; 102 | } 103 | } 104 | } 105 | Ok(()) 106 | } 107 | 108 | pub fn add_pseudo_proc(&mut self, exe: &Executable) { 109 | // TODO: using symlink would be better 110 | self.add_file_from(BundlePath::new("proc/self/exe"), exe.path()); 111 | } 112 | 113 | pub fn create_jail(&self) -> Result { 114 | let jail = Jail::new()?; 115 | tracing::debug!(path = %jail.path().display(), "bundle: created jail"); 116 | 117 | self.emit(&jail)?; 118 | Ok(jail) 119 | } 120 | } 121 | 122 | fn create_parent_dir

(path: P) -> Result<()> 123 | where 124 | P: AsRef, 125 | { 126 | match path.as_ref().parent() { 127 | Some(parent) if !parent.exists() => fs::create_dir_all(parent).map_err(Into::into), 128 | _ => Ok(()), 129 | } 130 | } 131 | 132 | // We don't use `fs::copy` directly because we want to respect symlinks. 133 | // Also `fs::canonicalize` is not used because we don't want to skip intermediate links. 134 | fn sync_copy(from: &Path, to: &BundlePath, dest: &Path) -> Result<()> { 135 | use std::os::unix; 136 | debug_assert!(from.is_absolute()); 137 | debug_assert!(dest.is_absolute()); 138 | 139 | let target = to.reify(dest); 140 | debug_assert!(target.is_absolute()); 141 | 142 | create_parent_dir(&target)?; 143 | 144 | if !from.exists() { 145 | tracing::warn!( 146 | path = %from.display(), 147 | "emit: copy source does not exist. skipping.", 148 | ); 149 | return Ok(()); 150 | } 151 | 152 | if fs::symlink_metadata(from)?.file_type().is_symlink() { 153 | let link_dest = from.read_link()?; 154 | let link_dest_absolute = if link_dest.is_relative() { 155 | // unwrap is ok because `from` here is an absolute path to a symbolic link 156 | from.parent().unwrap().join(link_dest) 157 | } else { 158 | link_dest 159 | }; 160 | tracing::info!( 161 | link = %link_dest_absolute.display(), 162 | target = %target.display(), 163 | "emit: link", 164 | ); 165 | match target.read_link() { 166 | // the bundle may contain an entry that symlinks to `target` 167 | Ok(target_link_dest) if target_link_dest == link_dest_absolute => { 168 | tracing::debug!( 169 | link = %link_dest_absolute.display(), 170 | target = %target.display(), 171 | "emit: already linked, skipping", 172 | ); 173 | } 174 | _ => { 175 | unix::fs::symlink(&link_dest_absolute, target)?; 176 | } 177 | } 178 | sync_copy( 179 | &link_dest_absolute, 180 | BundlePath::projection(&link_dest_absolute), 181 | dest, 182 | ) 183 | } else { 184 | tracing::info!(from = %from.display(), target = %target.display(), "emit: copy"); 185 | fs::copy(from, target)?; 186 | Ok(()) 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | use assert_fs::prelude::*; 194 | use predicates::prelude::*; 195 | use std::os::unix; 196 | 197 | #[test] 198 | fn test_sync_copy() -> std::result::Result<(), Box> { 199 | let dest = assert_fs::TempDir::new()?; 200 | let src = assert_fs::NamedTempFile::new("x.txt")?; 201 | src.write_str("hello")?; 202 | let bundle_path = BundlePath::new("a/b/c.txt"); 203 | sync_copy(src.path(), bundle_path, dest.path())?; 204 | dest.child("a/b/c.txt").assert("hello"); 205 | Ok(()) 206 | } 207 | 208 | #[test] 209 | fn test_sync_copy_nonexistent() -> std::result::Result<(), Box> { 210 | let dest = assert_fs::TempDir::new()?; 211 | let src = assert_fs::TempDir::new()?.child("nonexistent.txt"); 212 | let bundle_path = BundlePath::new("a/b/c.txt"); 213 | sync_copy(src.path(), bundle_path, dest.path())?; 214 | dest.child("a/b/c.txt").assert(predicate::path::missing()); 215 | Ok(()) 216 | } 217 | 218 | #[test] 219 | fn test_sync_copy_link() -> std::result::Result<(), Box> { 220 | let dest = assert_fs::TempDir::new()?; 221 | let src_dir = assert_fs::TempDir::new()?; 222 | let src = src_dir.child("x.txt"); 223 | src.touch()?; 224 | src.write_str("hello")?; 225 | let link = src_dir.child("y.txt"); 226 | unix::fs::symlink(src.path(), link.path())?; 227 | 228 | let bundle_path = BundlePath::new("a/b/c.txt"); 229 | sync_copy(link.path(), bundle_path, dest.path())?; 230 | 231 | assert!(fs::symlink_metadata(dest.child("a/b/c.txt").path())? 232 | .file_type() 233 | .is_symlink()); 234 | // dest.child("a/b/c.txt") 235 | // .assert(predicate::path::is_symlink()); 236 | dest.child("a/b/c.txt").assert("hello"); 237 | 238 | let bundle_src_path = dest.child(src.path().strip_prefix("/").unwrap()); 239 | bundle_src_path.assert("hello"); 240 | bundle_src_path.assert(predicate::path::is_file()); 241 | Ok(()) 242 | } 243 | 244 | #[test] 245 | fn test_mkdir() -> std::result::Result<(), Box> { 246 | let dest = assert_fs::TempDir::new()?; 247 | let mut bundle = Bundle::new(); 248 | bundle.mkdir(BundlePath::new("dir/dirdir")); 249 | bundle.emit(dest.path())?; 250 | 251 | dest.child("dir/dirdir").assert(predicate::path::is_dir()); 252 | Ok(()) 253 | } 254 | 255 | #[test] 256 | fn test_add_file() -> std::result::Result<(), Box> { 257 | let dest = assert_fs::TempDir::new()?; 258 | let mut bundle = Bundle::new(); 259 | bundle.add_file(BundlePath::new("dir/text.txt"), b"hello".to_vec()); 260 | bundle.emit(dest.path())?; 261 | 262 | dest.child("dir/text.txt").assert("hello"); 263 | Ok(()) 264 | } 265 | 266 | #[test] 267 | fn test_add_file_from() -> std::result::Result<(), Box> { 268 | let dest = assert_fs::TempDir::new()?; 269 | let src = assert_fs::NamedTempFile::new("x.txt")?; 270 | src.write_str("hello")?; 271 | 272 | let mut bundle = Bundle::new(); 273 | bundle.add_file_from(BundlePath::new("dir/text.txt"), src.path()); 274 | bundle.emit(dest.path())?; 275 | 276 | dest.child("dir/text.txt").assert("hello"); 277 | Ok(()) 278 | } 279 | 280 | #[test] 281 | fn test_filter() -> std::result::Result<(), Box> { 282 | let dest = assert_fs::TempDir::new()?; 283 | 284 | let mut bundle = Bundle::new(); 285 | bundle.add_file(BundlePath::new("a.txt"), b"hello1".to_vec()); 286 | bundle.add_file(BundlePath::new("b.txt"), b"hello2".to_vec()); 287 | bundle.filter(|path| path.to_str_lossy().contains("a")); 288 | bundle.emit(dest.path())?; 289 | 290 | dest.child("a.txt").assert("hello1"); 291 | dest.child("b.txt").assert(predicate::path::missing()); 292 | Ok(()) 293 | } 294 | 295 | #[test] 296 | fn test_chained_sync_copy_link() -> std::result::Result<(), Box> { 297 | // z.txt -> y.txt -> x.txt 298 | // and more than one of these paths are added to bundle 299 | 300 | let dest = assert_fs::TempDir::new()?; 301 | 302 | let src_dir = assert_fs::TempDir::new()?; 303 | let src = src_dir.child("x.txt"); 304 | src.touch()?; 305 | src.write_str("hello")?; 306 | let link1 = src_dir.child("y.txt"); 307 | unix::fs::symlink(src.path(), link1.path())?; 308 | let link2 = src_dir.child("z.txt"); 309 | unix::fs::symlink(link1.path(), link2.path())?; 310 | 311 | sync_copy(src.path(), BundlePath::projection(&src), dest.path())?; 312 | sync_copy(link1.path(), BundlePath::projection(&link1), dest.path())?; 313 | sync_copy(link2.path(), BundlePath::projection(&link2), dest.path())?; 314 | 315 | assert!(matches!( 316 | dest.child(link1.path()).path().read_link(), 317 | Ok(link_dest) 318 | if link_dest == src.path() 319 | )); 320 | assert!(matches!( 321 | dest.child(link2.path()).path().read_link(), 322 | Ok(link_dest) 323 | if link_dest == link1.path() 324 | )); 325 | dest.child(src.path()) 326 | .assert(predicate::path::is_file()) 327 | .assert("hello"); 328 | Ok(()) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/domain/bundle_path.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::{Borrow, Cow}; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::fmt; 4 | use std::ops::Deref; 5 | use std::path::{Path, PathBuf}; 6 | 7 | pub struct BundlePath { 8 | inner: OsStr, 9 | } 10 | 11 | impl BundlePath { 12 | pub fn new(s: &S) -> &BundlePath 13 | where 14 | S: AsRef + ?Sized, 15 | { 16 | unsafe { &*(s.as_ref() as *const OsStr as *const BundlePath) } 17 | } 18 | 19 | pub fn projection<'a, P>(p: &'a P) -> &'a BundlePath 20 | where 21 | P: AsRef + 'a, 22 | { 23 | let path = p.as_ref().strip_prefix("/").unwrap_or_else(|_| p.as_ref()); 24 | BundlePath::new(path) 25 | } 26 | 27 | pub fn to_path_buf(&self) -> BundlePathBuf { 28 | BundlePathBuf { 29 | inner: self.inner.to_os_string(), 30 | } 31 | } 32 | 33 | pub fn to_str_lossy(&self) -> Cow { 34 | self.inner.to_string_lossy() 35 | } 36 | 37 | pub fn display(&self) -> Display<'_> { 38 | Display { inner: self } 39 | } 40 | 41 | pub fn reify

(&self, dist: P) -> PathBuf 42 | where 43 | P: AsRef, 44 | { 45 | dist.as_ref().join(&self.inner) 46 | } 47 | } 48 | 49 | impl ToOwned for BundlePath { 50 | type Owned = BundlePathBuf; 51 | 52 | fn to_owned(&self) -> BundlePathBuf { 53 | self.to_path_buf() 54 | } 55 | } 56 | 57 | impl AsRef for BundlePath { 58 | fn as_ref(&self) -> &BundlePath { 59 | self 60 | } 61 | } 62 | 63 | #[derive(PartialEq, Eq, Hash, Clone)] 64 | pub struct BundlePathBuf { 65 | inner: OsString, 66 | } 67 | 68 | impl Deref for BundlePathBuf { 69 | type Target = BundlePath; 70 | 71 | fn deref(&self) -> &BundlePath { 72 | BundlePath::new(&self.inner) 73 | } 74 | } 75 | 76 | impl AsRef for BundlePathBuf { 77 | fn as_ref(&self) -> &BundlePath { 78 | self 79 | } 80 | } 81 | 82 | impl Borrow for BundlePathBuf { 83 | fn borrow(&self) -> &BundlePath { 84 | self.deref() 85 | } 86 | } 87 | 88 | pub struct Display<'a> { 89 | inner: &'a BundlePath, 90 | } 91 | 92 | impl<'a> fmt::Display for Display<'a> { 93 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 94 | write!(f, "[{}]", Path::new(&self.inner.inner).display()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/domain/executable.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::ffi::OsStr; 3 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | use std::{env, fs}; 7 | 8 | use crate::base::log::CommandLogExt; 9 | use crate::base::{Error, Result}; 10 | 11 | use goblin::elf::dynamic::{Dyn, DT_RPATH, DT_RUNPATH}; 12 | use goblin::elf::Elf; 13 | use goblin::strtab::Strtab; 14 | use tempfile::{NamedTempFile, TempPath}; 15 | 16 | mod resolver; 17 | mod search_paths; 18 | use search_paths::SearchPaths; 19 | 20 | #[derive(Debug)] 21 | enum ExecutableLocation { 22 | Fixed(PathBuf), 23 | Temporary(TempPath), 24 | } 25 | 26 | impl AsRef for ExecutableLocation { 27 | fn as_ref(&self) -> &Path { 28 | match self { 29 | ExecutableLocation::Fixed(path) => path.as_ref(), 30 | ExecutableLocation::Temporary(temp_path) => temp_path.as_ref(), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct Executable { 37 | location: ExecutableLocation, 38 | name: String, 39 | interpreter: Option, 40 | libraries: Vec, 41 | search_paths: SearchPaths, 42 | } 43 | 44 | impl Executable { 45 | fn load_impl( 46 | location: ExecutableLocation, 47 | name: String, 48 | propagated_rpaths: Option>, 49 | ) -> Result { 50 | tracing::debug!(location = %location.as_ref().display(), "exe: loading"); 51 | let buffer = fs::read(location.as_ref())?; 52 | let elf = Elf::parse(buffer.as_slice())?; 53 | let interpreter = if let Some(interp) = elf.interpreter { 54 | Some(interp.into()) 55 | } else { 56 | let interp = default_interpreter(&location)?; 57 | if let Some(interp) = &interp { 58 | tracing::debug!(interp = %interp.display(), "exe: using default interpreter"); 59 | } else { 60 | tracing::warn!( 61 | "exe: interpreter could not be found. static or compressed executable?" 62 | ); 63 | } 64 | interp 65 | }; 66 | let mut search_paths = collect_paths(&elf, location.as_ref())?; 67 | let libraries = elf.libraries.into_iter().map(ToOwned::to_owned).collect(); 68 | 69 | if let Some(paths) = propagated_rpaths { 70 | search_paths.append_rpath(paths); 71 | } 72 | 73 | let exe = Executable { 74 | location, 75 | name, 76 | interpreter, 77 | libraries, 78 | search_paths, 79 | }; 80 | 81 | tracing::debug!(exe = ?exe, "exe: loaded"); 82 | Ok(exe) 83 | } 84 | 85 | fn load_with_rpaths

(exe_path: P, propagated_rpaths: Option>) -> Result 86 | where 87 | P: AsRef, 88 | { 89 | let path = exe_path.as_ref(); 90 | 91 | let location = ExecutableLocation::Fixed(path.to_owned()); 92 | let file_name = path 93 | .file_name() 94 | .ok_or_else(|| Error::InvalidObjectPath(path.to_owned()))?; 95 | let file_name_str = file_name 96 | .to_str() 97 | .ok_or_else(|| Error::PathEncoding(file_name.to_os_string()))? 98 | .to_string(); 99 | Executable::load_impl(location, file_name_str, propagated_rpaths) 100 | } 101 | 102 | pub fn load

(exe_path: P) -> Result 103 | where 104 | P: AsRef, 105 | { 106 | Executable::load_with_rpaths(exe_path, None) 107 | } 108 | 109 | pub fn path(&self) -> &Path { 110 | self.location.as_ref() 111 | } 112 | 113 | pub fn name(&self) -> &String { 114 | &self.name 115 | } 116 | 117 | pub fn interpreter(&self) -> Option<&PathBuf> { 118 | self.interpreter.as_ref() 119 | } 120 | 121 | fn dynamic_libraries_impl

( 122 | &self, 123 | resolving_libraries: &mut HashSet, 124 | cc_path: P, 125 | is_noload: bool, 126 | ) -> Result> 127 | where 128 | P: AsRef, 129 | { 130 | let interpreter = if let Some(interp) = &self.interpreter { 131 | interp 132 | } else { 133 | tracing::warn!( 134 | "exe: requesting dynamic libraries of the executable without the interpreter" 135 | ); 136 | return Ok(Vec::new()); 137 | }; 138 | 139 | let resolver = if is_noload { 140 | resolver::Resolver::new_noload(interpreter, &self.search_paths, cc_path.as_ref())? 141 | } else { 142 | resolver::Resolver::new(interpreter, &self.search_paths, cc_path.as_ref())? 143 | }; 144 | 145 | let mut paths = Vec::new(); 146 | for lib in &self.libraries { 147 | let path = resolver.lookup(lib)?; 148 | tracing::debug!( 149 | name = %lib, 150 | path = %path.display(), 151 | "exe: found shared object", 152 | ); 153 | 154 | if !resolving_libraries.contains(lib) { 155 | resolving_libraries.insert(lib.to_owned()); 156 | // TODO: cache once traversed 157 | // TODO: deal with semantic inconsistency (Executable on shared object) 158 | let mut children = 159 | Executable::load_with_rpaths(path.clone(), self.search_paths.rpath().cloned())? 160 | .dynamic_libraries_impl(resolving_libraries, cc_path.as_ref(), is_noload)?; 161 | 162 | paths.push(path); 163 | paths.append(&mut children); 164 | } 165 | } 166 | 167 | Ok(paths) 168 | } 169 | 170 | pub fn dynamic_libraries

(&self, cc_path: P) -> Result> 171 | where 172 | P: AsRef, 173 | { 174 | let mut resolving_libraries = HashSet::new(); 175 | self.dynamic_libraries_impl(&mut resolving_libraries, cc_path, false) 176 | } 177 | 178 | pub fn dynamic_libraries_noload

(&self, cc_path: P) -> Result> 179 | where 180 | P: AsRef, 181 | { 182 | let mut resolving_libraries = HashSet::new(); 183 | self.dynamic_libraries_impl(&mut resolving_libraries, cc_path, true) 184 | } 185 | 186 | pub fn compressed(&self, upx_path: P, upx_opts: I) -> Result 187 | where 188 | P: AsRef, 189 | I: IntoIterator, 190 | T: AsRef, 191 | { 192 | let result_path = NamedTempFile::new()?.into_temp_path(); 193 | 194 | // NOTE: We use `TempPath` to delete it in `Drop::drop`, and TempPath can be obtained from `NamedTempFile`. 195 | // However, upx requires nonexistent output path. So we delete it once here. 196 | // NOTE: We expect `fs::remove_file` to remove the file immediately, though the 197 | // documentation says 'there is no guarantee that the file is immediately deleted'. 198 | fs::remove_file(&result_path)?; 199 | assert!(!result_path.exists()); 200 | let output = Command::new(upx_path.as_ref()) 201 | .args(upx_opts) 202 | .arg("--no-progress") 203 | .arg(self.path().canonicalize()?) 204 | .arg("-o") 205 | .arg(&result_path) 206 | .output_with_log()?; 207 | 208 | if !output.status.success() { 209 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 210 | return Err(Error::Upx(stderr)); 211 | } 212 | 213 | Executable::load_impl( 214 | ExecutableLocation::Temporary(result_path), 215 | self.name().clone(), 216 | None, 217 | ) 218 | } 219 | } 220 | 221 | fn default_interpreter

(exe: P) -> Result> 222 | where 223 | P: AsRef, 224 | { 225 | // from the source code of ldd(1); TODO: deal with hardcoded paths 226 | let rtld_list = &[ 227 | "/usr/lib/ld-linux.so.2", 228 | "/usr/lib64/ld-linux-x86-64.so.2", 229 | "/usr/libx32/ld-linux-x32.so.2", 230 | "/lib/ld-linux.so.2", 231 | "/lib64/ld-linux-x86-64.so.2", 232 | "/libx32/ld-linux-x32.so.2", 233 | ]; 234 | for rtld in rtld_list { 235 | let path = Path::new(rtld); 236 | if !path.exists() { 237 | continue; 238 | } 239 | 240 | let status = Command::new(rtld) 241 | .arg("--verify") 242 | .arg(exe.as_ref()) 243 | .status_with_log()?; 244 | match status.code() { 245 | Some(0) | Some(2) => return Ok(Some(rtld.into())), 246 | _ => continue, 247 | } 248 | } 249 | 250 | Ok(None) 251 | } 252 | 253 | fn collect_paths(elf: &Elf<'_>, executable_path: &Path) -> Result { 254 | debug_assert!(executable_path.is_absolute()); 255 | // unwrap is ok here because the path points to file and is absolute 256 | let origin = executable_path.parent().unwrap(); 257 | let mut paths = SearchPaths::new(origin.into())?; 258 | 259 | if let Elf { 260 | dynamic: Some(dynamic), 261 | dynstrtab, 262 | .. 263 | } = elf 264 | { 265 | for d in &dynamic.dyns { 266 | if d.d_tag == DT_RUNPATH { 267 | paths.append_runpath(get_paths_in_strtab(d, dynstrtab)?); 268 | } else if d.d_tag == DT_RPATH { 269 | paths.append_rpath(get_paths_in_strtab(d, dynstrtab)?); 270 | } 271 | } 272 | } 273 | 274 | if let Some(paths_str) = env::var_os("LD_LIBRARY_PATH") { 275 | tracing::debug!( 276 | value = %paths_str.to_string_lossy(), 277 | "executable: got LD_LIBRARY_PATH", 278 | ); 279 | 280 | paths.append_ld_library_path( 281 | paths_str 282 | .into_vec() 283 | .split(|b| *b == b':' || *b == b';') 284 | .map(OsStr::from_bytes), 285 | ); 286 | } 287 | 288 | Ok(paths) 289 | } 290 | 291 | fn get_paths_in_strtab(d: &Dyn, strtab: &Strtab<'_>) -> Result> { 292 | let content = get_content_in_strtab(d, strtab)?; 293 | // assuming paths in DT_RPATH and DT_RUNPATH are separated by colons. 294 | Ok(content.split(':').map(Into::into).collect()) 295 | } 296 | 297 | fn get_content_in_strtab(d: &Dyn, strtab: &Strtab<'_>) -> Result { 298 | if let Some(x) = strtab.get_at(d.d_val as usize) { 299 | Ok(x.to_owned()) 300 | } else { 301 | Err(Error::ValueNotFoundInStrtab { 302 | tag: d.d_tag, 303 | val: d.d_val, 304 | }) 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/domain/executable/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::path::{Path, PathBuf}; 3 | use std::process::Command; 4 | use std::{env, str}; 5 | 6 | use crate::base::log::CommandLogExt; 7 | use crate::base::{Error, Result}; 8 | use crate::domain::executable::SearchPaths; 9 | 10 | use tempfile::NamedTempFile; 11 | 12 | static GENERIC_RESOLVER_SOURCE_CODE: &str = r" 13 | #define _GNU_SOURCE 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | int main(int argc, char** argv) { 20 | char* name = argv[1]; 21 | void* handle = dlopen(name, RTLD_LAZY); 22 | if (handle == NULL) { 23 | fputs(dlerror(), stderr); 24 | return 1; 25 | } 26 | struct link_map* link_map; 27 | if (dlinfo(handle, RTLD_DI_LINKMAP, &link_map) != 0) { 28 | fputs(dlerror(), stderr); 29 | return 2; 30 | } 31 | puts(link_map->l_name); 32 | dlclose(handle); 33 | }"; 34 | 35 | static NOLOAD_RESOLVER_SOURCE_CODE: &str = r" 36 | #define _GNU_SOURCE 37 | #include 38 | #include 39 | 40 | #include 41 | 42 | int main(int argc, char** argv) { 43 | char* name = argv[1]; 44 | void* handle = dlopen(name, RTLD_LAZY | RTLD_NOLOAD); 45 | if (handle == NULL) { 46 | fputs(dlerror(), stderr); 47 | return 1; 48 | } 49 | struct link_map* link_map; 50 | if (dlinfo(handle, RTLD_DI_LINKMAP, &link_map) != 0) { 51 | fputs(dlerror(), stderr); 52 | return 2; 53 | } 54 | puts(link_map->l_name); 55 | dlclose(handle); 56 | }"; 57 | 58 | #[derive(Debug)] 59 | enum ResolverProgram { 60 | NoLoad { interp: PathBuf, cc_path: PathBuf }, 61 | Generic { program_path: PathBuf }, 62 | } 63 | 64 | impl ResolverProgram { 65 | pub fn new(interp: P, cc_path: Q) -> Result 66 | where 67 | P: AsRef, 68 | Q: AsRef, 69 | { 70 | let program_path = calc_resolver_program_path(&interp, &cc_path, "generic"); 71 | if !program_path.exists() { 72 | build_generic_resolver_program(&program_path, &interp, &cc_path)?; 73 | } 74 | 75 | Ok(ResolverProgram::Generic { program_path }) 76 | } 77 | 78 | pub fn new_noload(interp: P, cc_path: Q) -> Self 79 | where 80 | P: AsRef, 81 | Q: AsRef, 82 | { 83 | ResolverProgram::NoLoad { 84 | interp: interp.as_ref().to_owned(), 85 | cc_path: cc_path.as_ref().to_owned(), 86 | } 87 | } 88 | 89 | pub fn setup_for(&self, name: &str) -> Result { 90 | match self { 91 | ResolverProgram::NoLoad { interp, cc_path } => { 92 | let program_path = calc_resolver_program_path(interp, cc_path, name); 93 | if !program_path.exists() { 94 | build_noload_resolver_program(&program_path, interp, cc_path, name)?; 95 | } 96 | Ok(program_path) 97 | } 98 | ResolverProgram::Generic { program_path } => Ok(program_path.clone()), 99 | } 100 | } 101 | } 102 | 103 | #[derive(Debug)] 104 | pub struct Resolver<'a> { 105 | search_paths: &'a SearchPaths, 106 | program: ResolverProgram, 107 | } 108 | 109 | impl<'a> Resolver<'a> { 110 | pub fn new(interp: P, search_paths: &'a SearchPaths, cc_path: Q) -> Result 111 | where 112 | P: AsRef, 113 | Q: AsRef, 114 | { 115 | let resolver = Resolver { 116 | search_paths, 117 | program: ResolverProgram::new(interp, cc_path)?, 118 | }; 119 | 120 | tracing::debug!(?resolver, "resolver: created resolver"); 121 | Ok(resolver) 122 | } 123 | 124 | pub fn new_noload(interp: P, search_paths: &'a SearchPaths, cc_path: Q) -> Result 125 | where 126 | P: AsRef, 127 | Q: AsRef, 128 | { 129 | let resolver = Resolver { 130 | search_paths, 131 | program: ResolverProgram::new_noload(interp, cc_path), 132 | }; 133 | 134 | tracing::debug!(?resolver, "resolver: created resolver"); 135 | Ok(resolver) 136 | } 137 | 138 | // lookup_rpath --> lookup_env --> lookup_runpath --> lookup_rest 139 | // TODO: take secure-execution mode into consideration 140 | pub fn lookup(&self, name: &str) -> Result { 141 | if let Some(path) = self.lookup_rpath(name) { 142 | tracing::debug!(%name, path = %path.display(), "resolver: found by Rpath"); 143 | return Ok(path); 144 | } 145 | 146 | if let Some(path) = self.lookup_env(name) { 147 | tracing::debug!( 148 | %name, 149 | path = %path.display(), 150 | "resolver: found by LD_LIBRARY_PATH", 151 | ); 152 | return Ok(path); 153 | } 154 | 155 | if let Some(path) = self.lookup_runpath(name) { 156 | tracing::debug!(%name, path = %path.display(), "resolver: found by RunPath"); 157 | return Ok(path); 158 | } 159 | 160 | let path = self.lookup_rest(name)?; 161 | tracing::debug!(%name, path = %path.display(), "resolver: found by ld.so"); 162 | 163 | Ok(path) 164 | } 165 | 166 | fn lookup_rpath(&self, name: &str) -> Option { 167 | if self.search_paths.runpath().is_some() { 168 | return None; 169 | } 170 | 171 | self.search_paths 172 | .iter_rpaths() 173 | .find_map(|x| try_joined(x, name)) 174 | } 175 | 176 | fn lookup_runpath(&self, name: &str) -> Option { 177 | self.search_paths 178 | .iter_runpaths() 179 | .find_map(|x| try_joined(x, name)) 180 | } 181 | 182 | fn lookup_env(&self, name: &str) -> Option { 183 | self.search_paths 184 | .iter_ld_library_paths() 185 | .find_map(|x| try_joined(x, name)) 186 | } 187 | 188 | fn lookup_rest(&self, name: &str) -> Result { 189 | let program_path = self.program.setup_for(name)?; 190 | let output = Command::new(&program_path) 191 | .arg(name) 192 | .env_clear() 193 | .output_with_log()?; 194 | if !output.status.success() { 195 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 196 | return Err(Error::SharedLibraryLookup(stderr)); 197 | } 198 | 199 | Ok(str::from_utf8(&output.stdout)?.trim().to_string().into()) 200 | } 201 | } 202 | 203 | fn calc_resolver_program_path(interp: P, cc_path: Q, name: &str) -> PathBuf 204 | where 205 | P: AsRef, 206 | Q: AsRef, 207 | { 208 | use std::collections::hash_map::DefaultHasher; 209 | use std::hash::{Hash, Hasher}; 210 | 211 | let interp_hash = { 212 | let mut s = DefaultHasher::new(); 213 | interp.as_ref().hash(&mut s); 214 | s.finish() 215 | }; 216 | 217 | let cc_path_hash = { 218 | let mut s = DefaultHasher::new(); 219 | cc_path.as_ref().hash(&mut s); 220 | s.finish() 221 | }; 222 | 223 | let mut path = env::temp_dir(); 224 | path.push(format!( 225 | "magicpak_resolver_{}_{}_{}", 226 | name, interp_hash, cc_path_hash 227 | )); 228 | path 229 | } 230 | 231 | fn build_generic_resolver_program(program_path: P, interp: Q, cc_path: R) -> Result<()> 232 | where 233 | P: AsRef, 234 | Q: AsRef, 235 | R: AsRef, 236 | { 237 | let mut source = NamedTempFile::new()?; 238 | write!(source, "{}", GENERIC_RESOLVER_SOURCE_CODE)?; 239 | let source_path = source.into_temp_path(); 240 | 241 | let output = Command::new(cc_path.as_ref()) 242 | .arg("-xc") 243 | .arg(&source_path) 244 | .arg(format!("-Wl,-dynamic-linker,{}", interp.as_ref().display())) 245 | .arg("-ldl") 246 | .arg("-o") 247 | .arg(program_path.as_ref()) 248 | .output_with_log()?; 249 | if !output.status.success() { 250 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 251 | return Err(Error::ResolverCompilation(stderr)); 252 | } 253 | source_path.close()?; 254 | Ok(()) 255 | } 256 | 257 | fn build_noload_resolver_program( 258 | program_path: P, 259 | interp: Q, 260 | cc_path: R, 261 | name: &str, 262 | ) -> Result<()> 263 | where 264 | P: AsRef, 265 | Q: AsRef, 266 | R: AsRef, 267 | { 268 | let mut source = NamedTempFile::new()?; 269 | write!(source, "{}", NOLOAD_RESOLVER_SOURCE_CODE)?; 270 | let source_path = source.into_temp_path(); 271 | 272 | let output = Command::new(cc_path.as_ref()) 273 | .arg("-xc") 274 | .arg(&source_path) 275 | .arg(format!("-Wl,-dynamic-linker,{}", interp.as_ref().display())) 276 | .arg("-ldl") 277 | .arg("-Wl,--no-as-needed") 278 | .arg(format!("-l:{}", name)) 279 | .arg("-o") 280 | .arg(program_path.as_ref()) 281 | .output_with_log()?; 282 | if !output.status.success() { 283 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 284 | return Err(Error::ResolverCompilation(stderr)); 285 | } 286 | source_path.close()?; 287 | Ok(()) 288 | } 289 | 290 | fn try_joined(path1: P, path2: Q) -> Option 291 | where 292 | P: AsRef, 293 | Q: AsRef, 294 | { 295 | let joined = path1.as_ref().join(path2); 296 | if joined.exists() { 297 | Some(joined) 298 | } else { 299 | None 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/domain/executable/search_paths.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::io; 3 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use crate::base::{Error, Result}; 7 | 8 | #[derive(Hash, Default, Debug)] 9 | pub struct SearchPaths { 10 | rpath: Option>, 11 | runpath: Option>, 12 | ld_library_path: Option>, 13 | platform: OsString, 14 | origin: OsString, 15 | } 16 | 17 | impl SearchPaths { 18 | pub fn new(origin: OsString) -> Result { 19 | Ok(SearchPaths { 20 | rpath: None, 21 | runpath: None, 22 | ld_library_path: None, 23 | platform: auxv_platform()?, 24 | origin, 25 | }) 26 | } 27 | 28 | pub fn rpath(&self) -> Option<&Vec> { 29 | self.rpath.as_ref() 30 | } 31 | 32 | pub fn runpath(&self) -> Option<&Vec> { 33 | self.runpath.as_ref() 34 | } 35 | 36 | pub fn iter_rpaths(&self) -> impl Iterator { 37 | self.rpath.iter().flat_map(|v| v.iter().map(AsRef::as_ref)) 38 | } 39 | 40 | pub fn iter_runpaths(&self) -> impl Iterator { 41 | self.runpath 42 | .iter() 43 | .flat_map(|v| v.iter().map(AsRef::as_ref)) 44 | } 45 | 46 | pub fn iter_ld_library_paths(&self) -> impl Iterator { 47 | self.ld_library_path 48 | .iter() 49 | .flat_map(|v| v.iter().map(AsRef::as_ref)) 50 | } 51 | 52 | fn append(paths: &mut Option>, other: I, origin: &OsStr, platform: &OsStr) 53 | where 54 | I: IntoIterator, 55 | S: AsRef, 56 | { 57 | let inner = paths.get_or_insert(Vec::new()); 58 | inner.extend( 59 | other 60 | .into_iter() 61 | .map(|x| expand_tokens(x, origin, platform)), 62 | ) 63 | } 64 | 65 | pub fn append_rpath(&mut self, rpath: I) 66 | where 67 | I: IntoIterator, 68 | S: AsRef, 69 | { 70 | let SearchPaths { 71 | origin, platform, .. 72 | } = self; 73 | 74 | Self::append(&mut self.rpath, rpath, origin, platform) 75 | } 76 | 77 | pub fn append_runpath(&mut self, runpath: I) 78 | where 79 | I: IntoIterator, 80 | S: AsRef, 81 | { 82 | let SearchPaths { 83 | origin, platform, .. 84 | } = self; 85 | 86 | Self::append(&mut self.runpath, runpath, origin, platform) 87 | } 88 | 89 | pub fn append_ld_library_path(&mut self, ld_library_path: I) 90 | where 91 | I: IntoIterator, 92 | S: AsRef, 93 | { 94 | let SearchPaths { 95 | origin, platform, .. 96 | } = self; 97 | 98 | Self::append(&mut self.ld_library_path, ld_library_path, origin, platform) 99 | } 100 | } 101 | 102 | fn expand_tokens(input: S, origin: T, platform: U) -> PathBuf 103 | where 104 | S: AsRef, 105 | T: AsRef, 106 | U: AsRef, 107 | { 108 | let input = input.as_ref(); 109 | 110 | let mut result = OsString::new(); 111 | result.reserve(input.len()); 112 | 113 | let mut buffer = Vec::new(); 114 | buffer.reserve(16); 115 | 116 | enum ParseState { 117 | Standby, 118 | Scanning, 119 | ScanningBraced, 120 | } 121 | 122 | let state = input 123 | .as_bytes() 124 | .iter() 125 | .fold(ParseState::Standby, |state, b| match (state, b) { 126 | (ParseState::Standby, b'$') => { 127 | result.push(OsStr::from_bytes(&buffer)); 128 | buffer.clear(); 129 | ParseState::Scanning 130 | } 131 | (ParseState::Scanning, b'{') if buffer.is_empty() => ParseState::ScanningBraced, 132 | (ParseState::Scanning, b'$') => { 133 | result.push(substitute(&buffer, &origin, &platform)); 134 | buffer.clear(); 135 | 136 | ParseState::Scanning 137 | } 138 | (ParseState::Scanning, b'/') => { 139 | result.push(substitute(&buffer, &origin, &platform)); 140 | buffer.clear(); 141 | 142 | buffer.push(*b); 143 | ParseState::Standby 144 | } 145 | (ParseState::ScanningBraced, b'}') => { 146 | result.push(substitute(&buffer, &origin, &platform)); 147 | buffer.clear(); 148 | ParseState::Standby 149 | } 150 | (s, b) => { 151 | buffer.push(*b); 152 | s 153 | } 154 | }); 155 | 156 | match state { 157 | ParseState::ScanningBraced => { 158 | tracing::warn!( 159 | token = ?String::from_utf8_lossy(&buffer), 160 | "search_paths: unterminated braced token", 161 | ); 162 | result.push(OsStr::from_bytes(&buffer)); 163 | } 164 | ParseState::Scanning => { 165 | result.push(substitute(&buffer, &origin, &platform)); 166 | } 167 | ParseState::Standby => { 168 | result.push(OsStr::from_bytes(&buffer)); 169 | } 170 | } 171 | 172 | if input != result { 173 | tracing::info!( 174 | input = %input.to_string_lossy(), 175 | result = %result.to_string_lossy(), 176 | "search_paths: expand", 177 | ); 178 | } 179 | 180 | result.into() 181 | } 182 | 183 | fn substitute(s: &[u8], origin: S, platform: T) -> OsString 184 | where 185 | S: AsRef, 186 | T: AsRef, 187 | { 188 | match s { 189 | b"ORIGIN" => origin.as_ref().to_owned(), 190 | b"LIB" => match is_64bit(&platform) { 191 | Some(true) => OsStr::new("lib64").to_owned(), 192 | Some(false) => OsStr::new("lib").to_owned(), 193 | None => { 194 | tracing::warn!( 195 | platform = %platform.as_ref().to_string_lossy(), 196 | "search_paths: assuming 32-bit platform", 197 | ); 198 | OsStr::new("lib").to_owned() 199 | } 200 | }, 201 | b"PLATFORM" => platform.as_ref().to_owned(), 202 | _ => { 203 | tracing::warn!( 204 | token = %format!("${}", String::from_utf8_lossy(s)), 205 | "search_paths: unknown dynamic string token", 206 | ); 207 | OsString::from_vec([&[b'$'], s].concat().to_vec()) 208 | } 209 | } 210 | } 211 | 212 | fn is_64bit(platform: S) -> Option 213 | where 214 | S: AsRef, 215 | { 216 | match platform.as_ref().to_string_lossy().as_ref() { 217 | "x86_64" | "amd64" | "aarch64" => Some(true), 218 | "i386" | "i686" | "x86" | "arm" => Some(false), 219 | _ => None, 220 | } 221 | } 222 | 223 | fn auxv_platform() -> Result { 224 | let mut reader = crt0stack::Reader::from_environ().done(); 225 | let platform = reader 226 | .find_map(|entry| { 227 | if let crt0stack::Entry::Platform(platform) = entry { 228 | Some(platform) 229 | } else { 230 | None 231 | } 232 | }) 233 | .ok_or_else(|| { 234 | Error::IO(io::Error::new( 235 | io::ErrorKind::NotFound, 236 | "could not find AT_PLATFORM auxval", 237 | )) 238 | })?; 239 | tracing::debug!(%platform, "search_paths: read platform from auxv"); 240 | Ok(platform.into()) 241 | } 242 | 243 | #[cfg(test)] 244 | mod tests { 245 | use super::*; 246 | 247 | #[test] 248 | fn test_substitute() { 249 | let origin = "/home/user/"; 250 | let platform = "x86_64"; 251 | assert_eq!(substitute(b"ORIGIN", origin, platform), origin); 252 | assert_eq!(substitute(b"PLATFORM", origin, platform), platform); 253 | 254 | assert_eq!(substitute(b"LIB", origin, "x86_64"), "lib64"); 255 | assert_eq!(substitute(b"LIB", origin, "x86"), "lib"); 256 | 257 | assert_eq!(substitute(b"WTF", origin, platform), "$WTF"); 258 | } 259 | 260 | #[test] 261 | fn test_expand_tokens() { 262 | let origin = "/home/user/"; 263 | let platform = "x86_64"; 264 | assert_eq!( 265 | expand_tokens("$ORIGIN/$LIB", origin, platform), 266 | PathBuf::from(format!("{}/lib64", origin)) 267 | ); 268 | assert_eq!( 269 | expand_tokens("${ORIGIN}/${LIB}", origin, platform), 270 | PathBuf::from(format!("{}/lib64", origin)) 271 | ); 272 | assert_eq!( 273 | expand_tokens("/$ORIGIN$LIB", origin, platform), 274 | PathBuf::from(format!("/{}lib64", origin)) 275 | ); 276 | assert_eq!( 277 | expand_tokens("/lib/$PLATFORM", origin, platform), 278 | PathBuf::from(format!("/lib/{}", platform)) 279 | ); 280 | assert_eq!( 281 | expand_tokens("${PLATFORM}${ORIGIN}", origin, platform), 282 | PathBuf::from(format!("{}{}", platform, origin)) 283 | ); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/domain/jail.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::fs::PermissionsExt; 2 | use std::os::unix::process::CommandExt; 3 | use std::path::Path; 4 | use std::process::Command; 5 | use std::{env, fs}; 6 | 7 | use crate::base::log::CommandLogExt; 8 | use crate::base::{Error, Result}; 9 | 10 | use tempfile::TempDir; 11 | 12 | pub struct Jail { 13 | dir: TempDir, 14 | } 15 | 16 | impl Jail { 17 | pub fn new() -> Result { 18 | let dir = TempDir::new()?; 19 | Ok(Jail { dir }) 20 | } 21 | 22 | pub fn install_busybox

(&self, busybox_path: P) -> Result<()> 23 | where 24 | P: AsRef, 25 | { 26 | let bindir = self.dir.path().join("bin/"); 27 | let busybox_jail_path = bindir.join("busybox"); 28 | 29 | tracing::info!( 30 | from_path = %busybox_path.as_ref().display(), 31 | jail_path = %busybox_jail_path.display(), 32 | "jail: copying busybox", 33 | ); 34 | if !bindir.exists() { 35 | fs::create_dir(&bindir)?; 36 | } 37 | fs::copy(&busybox_path, &busybox_jail_path)?; 38 | fs::set_permissions(&busybox_jail_path, fs::Permissions::from_mode(0o755))?; 39 | 40 | let output = Command::new(busybox_jail_path) 41 | .arg("--install") 42 | .arg(bindir) 43 | .output_with_log()?; 44 | if !output.status.success() { 45 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 46 | return Err(Error::BusyBoxInstall(stderr)); 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | pub fn path(&self) -> &Path { 53 | self.dir.path() 54 | } 55 | } 56 | 57 | impl AsRef for Jail { 58 | fn as_ref(&self) -> &Path { 59 | self.path() 60 | } 61 | } 62 | 63 | pub trait CommandJailExt { 64 | fn in_jail(&mut self, jail: &Jail) -> &mut Self; 65 | } 66 | 67 | impl CommandJailExt for Command { 68 | fn in_jail(&mut self, jail: &Jail) -> &mut Command { 69 | let jail_path = jail.path().to_owned(); 70 | unsafe { 71 | self.pre_exec(move || { 72 | tracing::debug!(path = %jail_path.display(), "jail: chroot"); 73 | nix::unistd::chroot(&jail_path)?; 74 | tracing::debug!("jail: chdir to /"); 75 | env::set_current_dir("/") 76 | }) 77 | } 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use assert_cmd::prelude::*; 85 | use predicates::prelude::*; 86 | use std::path::PathBuf; 87 | use std::process::Command; 88 | 89 | fn locate_busybox() -> std::result::Result> { 90 | let path = if let Ok(path) = std::env::var("MAGICPAK_TEST_STATIC_BUSYBOX") { 91 | path.into() 92 | } else { 93 | which::which("busybox")? 94 | }; 95 | Ok(path) 96 | } 97 | 98 | #[test] 99 | fn test_install_busybox() -> std::result::Result<(), Box> { 100 | let jail = Jail::new()?; 101 | jail.install_busybox(locate_busybox()?)?; 102 | 103 | assert_eq!( 104 | true, 105 | predicate::path::is_file().eval(&jail.path().join("bin/busybox")) 106 | ); 107 | assert_eq!( 108 | true, 109 | predicate::path::is_file().eval(&jail.path().join("bin/sh")) 110 | ); 111 | Ok(()) 112 | } 113 | 114 | #[test] 115 | #[ignore] 116 | fn test_jail() -> std::result::Result<(), Box> { 117 | let jail = Jail::new()?; 118 | jail.install_busybox(locate_busybox()?)?; 119 | 120 | Command::new("pwd") 121 | .in_jail(&jail) 122 | .assert() 123 | .success() 124 | .stdout("/\n"); 125 | 126 | Command::new("ls") 127 | .in_jail(&jail) 128 | .assert() 129 | .success() 130 | .stdout("bin\n"); 131 | 132 | Ok(()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/domain/resource.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::domain::{Bundle, BundlePath}; 4 | 5 | pub trait Resource { 6 | fn bundle_to(self, bundle: &mut Bundle); 7 | } 8 | 9 | impl Resource for &Path { 10 | fn bundle_to(self, bundle: &mut Bundle) { 11 | bundle.add_file_from(BundlePath::projection(&self), self); 12 | } 13 | } 14 | 15 | impl Resource for PathBuf { 16 | fn bundle_to(self, bundle: &mut Bundle) { 17 | self.as_path().bundle_to(bundle); 18 | } 19 | } 20 | 21 | impl Resource for &PathBuf { 22 | fn bundle_to(self, bundle: &mut Bundle) { 23 | self.as_path().bundle_to(bundle); 24 | } 25 | } 26 | 27 | impl Resource for Option 28 | where 29 | R: Resource, 30 | { 31 | fn bundle_to(self, bundle: &mut Bundle) { 32 | if let Some(x) = self { 33 | x.bundle_to(bundle); 34 | } 35 | } 36 | } 37 | 38 | impl Resource for Vec 39 | where 40 | R: Resource, 41 | { 42 | fn bundle_to(self, bundle: &mut Bundle) { 43 | for x in self { 44 | x.bundle_to(bundle); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/magicpak.rs: -------------------------------------------------------------------------------- 1 | pub mod base; 2 | 3 | pub mod action; 4 | pub mod domain; 5 | --------------------------------------------------------------------------------