cargo-auditable-0.6.6/.cargo_vcs_info.json0000644000000001550000000000100140710ustar { "git": { "sha1": "83713a1c609b28d93660c64b8974b0226fe2c7a4" }, "path_in_vcs": "cargo-auditable" }cargo-auditable-0.6.6/CHANGELOG.md000064400000000000000000000074011046102023000144730ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.6.6] - 2024-11-24 ### Changed - Audit data is now injected when `--print` argument is passed to `rustc` if `--emit=link` is also present in the same invocation. This adds support for `cargo c` third-party subcommand. - When `--emit` argument is passed to `rustc`, audit data will only be injected if one of the values passed is `link`. This should avoid messing with modes that emit assembly or LLVM bitcode. - Upgraded to `object` crate from v0.30 to v0.36 in order to reduce the dependency footprint. ### Fixed - Arguments to `rustc` in the style of `--key=value` (as opposed to `--key value`) are now parsed correctly. This was never an issue in practice because Cargo passes the arguments we care about separated by space, not `=`. ## [0.6.5] - 2024-11-11 ### Added - Upgraded the `cargo_metadata` dependency to gain support for Rust 2024 edition ### Fixed - Fixed build on `riscv64-linux-android` target and certain custom RISC-V targets ## [0.6.4] - 2024-05-08 ### Added - LoongArch support ## [0.6.3] - 2024-05-03 ### Added - WebAssembly support ### Fixed - Pass the correct flag to MSVC link.exe to preserve the symbol containing audit data - This is not known to cause issues in practice - the symbol was preserved anyway, even with LTO. - Tests no longer fail on Rust 1.77 and later. The issue affected test code only. ### Changed - Refactored platform detection to be more robust ## [0.6.2] - 2024-02-19 ### Fixed - Fixed `cargo auditable` encoding a cyclic dependency graph under [certain conditions](https://github.com/rustsec/rustsec/issues/1043) - Fixed an integration test failing intermittently on recent Rust versions ### Changed - No longer attempt to add audit info if `--print` arguments are passed to `rustc`, which disable code generation - Print a more meaningful error when invoking `rustc` fails ## [0.6.1] - 2023-03-06 ### Added - A Unix manpage - An explanation of how the project relates to supply chain attacks to the README - Keywords to the Cargo manifest to make discovering the project easier ### Changed - Updated to `object` crate version 0.30 to enable packaging for Debian - Synced to the latest object writing code from the Rust compiler. This should improve support for very obscure architectures. ## [0.6.0] - 2022-12-07 ### Changed - A build with `cargo auditable` no longer fails when targeting an unsupported architecture. Instead a warning is printed. - The `CARGO` environment variable is now read and honored; calls to Cargo will go through the binary specified in this variable instead of just `cargo`. ### Added - Added documentation on using `cargo auditable` as a drop-in replacement for `cargo`. ### Fixed - Fixed build failures when the `RUSTC` environment variable or the `build.rustc` configuration option is set. ## [0.5.5] - 2022-12-01 ### Fixed - Long builds with `sccache` now work as expected. They require additional quirks compared to regular Cargo builds, see [#87](https://github.com/rust-secure-code/cargo-auditable/issues/87). - Note that `sccache` v0.3.1 or later is required even with this fix - earlier versions have a [bug](https://github.com/mozilla/sccache/issues/1274) that prevents them from working with `cargo auditable`. ## [0.5.4] - 2022-11-12 ### Changed - Updated README.md ## [0.5.3] - 2022-11-12 ### Fixed - `--offline`, `--locked`, `--frozen` and `--config` flags now work as expected. Previously they were not forwarded to `cargo metadata`, so it could still access the network, etc. ### Added - Re-introduced CHANGELOG.md cargo-auditable-0.6.6/Cargo.lock0000644000000301600000000000100120430ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "auditable-extract" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44371e9f9759dea49c42b6c6fe4c64ea216ee2af325a4524a7180823e00d3e7a" dependencies = [ "binfarce", "wasmparser", ] [[package]] name = "auditable-info" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6308af569780bb0abb0c142da9612acecf1edcecdab341a5ce7d2f2f7e55b14f" dependencies = [ "auditable-extract", "auditable-serde", "miniz_oxide", "serde_json", ] [[package]] name = "auditable-serde" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" dependencies = [ "semver", "serde", "serde_json", "topological-sort", ] [[package]] name = "binfarce" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18464ccbb85e5dede30d70cc7676dc9950a0fb7dbf595a43d765be9123c616a2" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "byteorder" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" [[package]] name = "camino" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] [[package]] name = "cargo-auditable" version = "0.6.6" dependencies = [ "auditable-info", "auditable-serde", "cargo_metadata", "miniz_oxide", "object", "pico-args", "serde", "serde_json", "wasm-gen", "which", ] [[package]] name = "cargo-platform" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", "thiserror", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", ] [[package]] name = "foldhash" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" [[package]] name = "hashbrown" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" dependencies = [ "foldhash", ] [[package]] name = "home" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys", ] [[package]] name = "indexmap" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "itoa" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "leb128" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", ] [[package]] name = "object" version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "crc32fast", "hashbrown", "indexmap", "memchr", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pico-args" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rustix" version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "syn" version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "wasm-gen" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b854b1461005a7b3365742310f7faa3cac3add809d66928c64a40c7e9e842ebb" dependencies = [ "byteorder", "leb128", ] [[package]] name = "wasmparser" version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c" dependencies = [ "bitflags", ] [[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", "home", "once_cell", "rustix", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" cargo-auditable-0.6.6/Cargo.toml0000644000000030150000000000100120650ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "cargo-auditable" version = "0.6.6" authors = ["Sergey \"Shnatsel\" Davidoff "] description = "Make production Rust binaries auditable" readme = "README.md" keywords = [ "security", "supply-chain", "sbom", "vulnerabilities", ] categories = [ "development-tools::cargo-plugins", "encoding", ] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" resolver = "1" [dependencies.auditable-serde] version = "0.8.0" [dependencies.cargo_metadata] version = "0.18" [dependencies.miniz_oxide] version = "0.8.0" [dependencies.object] version = "0.36" features = ["write"] default-features = false [dependencies.pico-args] version = "0.5" features = ["eq-separator"] [dependencies.serde] version = "1.0.147" [dependencies.serde_json] version = "1.0.57" [dependencies.wasm-gen] version = "0.1.4" [dev-dependencies.auditable-info] version = "0.9.0" features = ["wasm"] [dev-dependencies.cargo_metadata] version = "0.18" [dev-dependencies.which] version = "4.3.0" cargo-auditable-0.6.6/Cargo.toml.orig000064400000000000000000000017751046102023000155610ustar 00000000000000[package] name = "cargo-auditable" version = "0.6.6" edition = "2021" authors = ["Sergey \"Shnatsel\" Davidoff "] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" description = "Make production Rust binaries auditable" categories = ["development-tools::cargo-plugins", "encoding"] keywords = ["security", "supply-chain", "sbom", "vulnerabilities"] readme = "../README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] object = {version = "0.36", default-features = false, features = ["write"]} auditable-serde = {version = "0.8.0", path = "../auditable-serde"} miniz_oxide = {version = "0.8.0"} serde_json = "1.0.57" cargo_metadata = "0.18" pico-args = { version = "0.5", features = ["eq-separator"] } serde = "1.0.147" wasm-gen = "0.1.4" [dev-dependencies] cargo_metadata = "0.18" auditable-info = {version = "0.9.0", path = "../auditable-info", features = ["wasm"]} which = "4.3.0" cargo-auditable-0.6.6/README.md000064400000000000000000000152751046102023000141510ustar 00000000000000## cargo-auditable Know the exact crate versions used to build your Rust executable. Audit binaries for known bugs or security vulnerabilities in production, at scale, with zero bookkeeping. This works by embedding data about the dependency tree in JSON format into a dedicated linker section of the compiled executable. Linux, Windows and Mac OS are officially supported. [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) is also supported starting with v0.6.3. All other ELF targets should work, but are not tested on CI. The end goal is to get Cargo itself to encode this information in binaries. There is an RFC for an implementation within Cargo, for which this project paves the way: https://github.com/rust-lang/rfcs/pull/2801 ## Usage ```bash # Install the tools cargo install cargo-auditable cargo-audit # Build your project with dependency lists embedded in the binaries cargo auditable build --release # Scan the binary for vulnerabilities cargo audit bin target/release/your-project ``` `cargo auditable` works with any Cargo command. All arguments are passed to `cargo` as-is. ## Adoption Microsoft uses `cargo auditable` internally and maintains the [data extraction library for Go](https://github.com/microsoft/go-rustaudit). Multiple Linux distributions build their Rust packages with `cargo auditable`: [Alpine Linux](https://www.alpinelinux.org/), [NixOS](https://nixos.org/), [openSUSE](https://www.opensuse.org/), [Void Linux](https://voidlinux.org/) and [Chimera Linux](https://chimera-linux.org/). If you install packages from their repositories, you can audit them! ## FAQ ### Doesn't this bloat my binary? In a word, no. The embedded dependency list uses under 4kB even on large dependency trees with 400+ entries. This typically translates to between 1/1000 and 1/10,000 of the size of the binary. ### Can I make `cargo` always build with `cargo auditable`? Yes! For example, on Linux/macOS/etc add this to your `.bashrc`: ```bash alias cargo="cargo auditable" ``` If you're using a shell other than bash, or if using an alias is not an option, [see here.](REPLACING_CARGO.md) ### Is there any tooling to consume this data? #### Vulnerability reporting * [cargo audit](https://crates.io/crates/cargo-audit) v0.17.3+ can detect this data in binaries and report on vulnerabilities. See [here](https://github.com/rustsec/rustsec/tree/main/cargo-audit#cargo-audit-bin-subcommand) for details. * [trivy](https://github.com/aquasecurity/trivy) v0.31.0+ detects this data in binaries and reports on vulnerabilities. See the [v0.31.0 release notes](https://github.com/aquasecurity/trivy/discussions/2716) for an end-to-end example. #### Recovering the dependency list * [syft](https://github.com/anchore/syft) v0.53.0+ has experimental support for detecting this data in binaries. When used on images or directories, Rust audit support must be enabled by adding the `--catalogers all` CLI option, e.g `syft --catalogers all `. * [rust-audit-info](https://crates.io/crates/rust-audit-info) recovers the dependency list from a binary and prints it in JSON. ### Can I read this data using a tool written in a different language? Yes. The data format is designed for interoperability with alternative implementations. In fact, parsing it only takes [5 lines of Python](PARSING.md). See [here](PARSING.md) for documentation on parsing the data. Besides that, Syft can read it and convert it to a multitude of formats. `auditable2cdx` can convert it to CycloneDX, which is understood by most tools. This conversion lets you feed this data even to tools you cannot modify. ### What is the data format, exactly? The data format is described by the JSON schema [here](cargo-auditable.schema.json). The JSON is Zlib-compressed and placed in a linker section named `.dep-v0`. You can find more info about parsing it [here](PARSING.md). ### What about embedded platforms? Embedded platforms where you cannot spare a byte should not add anything in the executable. Instead they should record the hash of every executable in a database and associate the hash with its Cargo.lock, compiler and LLVM version, build date, etc. This would make for an excellent Cargo wrapper or plugin. Since that can be done in a 5-line shell script, writing that tool is left as an exercise to the reader. ### Does this impact reproducible builds? The data format is specifically designed not to disrupt reproducible builds. It contains no timestamps, and the generated JSON is sorted to make sure it is identical between compilations. If anything, this *helps* with reproducible builds, since you know all the versions for a given binary now. ### Does this disclose any sensitive information? No. All URLs and file paths are redacted, but the crate names and versions are recorded as-is. At present panic messages already disclose all this info and more. Also, chances are that you're legally obligated have to disclose use of specific open-source crates anyway, since MIT and many other licenses require it. ### What about recording the compiler version? The compiler itself [embeds it](https://github.com/rust-lang/rust/pull/97550) in v1.73 and later. On older versions it's already there in the debug info. On Unix you can run `strings your_executable | grep 'rustc version'` to see it. ### What about keeping track of versions of statically linked C libraries? Good question. I don't think they are exposed in any reasonable way right now. Would be a great addition, but not required for the initial launch. We can add it later in a backwards-compatible way. Adopting [the `-src` crate convention](https://internals.rust-lang.org/t/statically-linked-c-c-libraries/17175?u=shnatsel) would make it happen naturally, and will have other benefits as well, so that's probably the best route. ### Does this protect against supply chain attacks? No. Use [`cargo-vet`](https://github.com/mozilla/cargo-vet) or [`cargo-crev`](https://github.com/crev-dev/cargo-crev) for that. [Software Bills of Materials](https://en.wikipedia.org/wiki/Software_supply_chain) (SBOMs) do not prevent supply chain attacks. They cannot even be used to assess the impact of such an attack after it is discovered, because any malicious library worth its bytes will remove itself from the SBOM. This applies to nearly every language and build system, not just Rust and Cargo. Do not rely on SBOMs when dealing with supply chain attacks! ### What is blocking uplifting this into Cargo? The [RFC for this functionality in Cargo itself](https://github.com/rust-lang/rfcs/pull/2801) has been [postponed](https://github.com/rust-lang/rfcs/pull/2801#issuecomment-2122880841) by the Cargo team until the [more foundational SBOM RFC](https://github.com/rust-lang/rfcs/pull/3553) is implemented. cargo-auditable-0.6.6/cargo-auditable.1000064400000000000000000000124151046102023000157700ustar 00000000000000.TH CARGO-AUDITABLE 1 .SH NAME cargo\-auditable \- Embed a JSON formatted dependency tree into a dedicated linker section of the compiled executable .SH SYNOPSIS \fBcargo\-auditable\fR .SH DESCRIPTION Know the exact crate versions used to build your Rust executable. Audit binaries for known bugs or security vulnerabilities in production, at scale, with zero bookkeeping. This works by embedding data about the dependency tree in JSON format into a dedicated linker section of the compiled executable. Linux, Windows and Mac OS are officially supported. All other ELF targets should work, but are not tested on CI. WASM is currently not supported, but patches are welcome. The end goal is to get Cargo itself to encode this information in binaries. There is an RFC for an implementation within Cargo, for which this project paves the way: https://github.com/rust\-lang/rfcs/pull/2801 .SH USAGE cargo auditable works with any Cargo command. All arguments are passed to cargo as\-is. .SH FAQ Doesn't this bloat my binary? In a word, no. The embedded dependency list uses under 4kB even on large dependency trees with 400+ entries. This typically translates to between 1/1000 and 1/10,000 of the size of the binary. Can I make cargo always build with cargo auditable? Yes! For example, on Linux/macOS/etc add this to your .bashrc: alias cargo="cargo auditable" If you're using a shell other than bash, or if using an alias is not an option, see https://github.com/rust\-secure\-code/cargo\-auditable/blob/HEAD/REPLACING_CARGO.md. Is there any tooling to consume this data? Vulnerability reporting cargo audit v0.17.3+ can detect this data in binaries and report on vulnerabilities. See here for details. trivy v0.31.0+ detects this data in binaries and reports on vulnerabilities. See the v0.31.0 release notes for an end\-to\-end example. Recovering the dependency list syft v0.53.0+ has experimental support for detecting this data in binaries. When used on images or directories, Rust audit support must be enabled by adding the \-\-catalogers all CLI option, e.g syft \-\-catalogers all . rust\-audit\-info recovers the dependency list from a binary and prints it in JSON. It is also interoperable with existing tooling that consumes Cargo.lock via the JSON\-to\-TOML convertor. However, we recommend supporting the format natively; the format is designed to be very easy to parse, even if your language does not have a library for that yet. Can I read this data using a tool written in a different language? Yes. The data format is designed for interoperability with alternative implementations. In fact, parsing it only takes 5 lines of Python. See https://github.com/rust\-secure\-code/cargo\-auditable/blob/HEAD/PARSING.md for documentation on parsing the data. What is the data format, exactly? The data format is described by the JSON schema https://github.com/rust\-secure\-code/cargo\-auditable/blob/HEAD/cargo\-auditable.schema.json. The JSON is Zlib\-compressed and placed in a linker section named .dep\-v0. You can find more info about parsing it here. What about embedded platforms? Embedded platforms where you cannot spare a byte should not add anything in the executable. Instead they should record the hash of every executable in a database and associate the hash with its Cargo.lock, compiler and LLVM version, build date, etc. This would make for an excellent Cargo wrapper or plugin. Since that can be done in a 5\-line shell script, writing that tool is left as an exercise to the reader. Does this impact reproducible builds? The data format is specifically designed not to disrupt reproducible builds. It contains no timestamps, and the generated JSON is sorted to make sure it is identical between compilations. If anything, this helps with reproducible builds, since you know all the versions for a given binary now. Does this disclose any sensitive information? No. All URLs and file paths are redacted, but the crate names and versions are recorded as\-is. At present panic messages already disclose all this info and more. Also, chances are that you're legally obligated have to disclose use of specific open\-source crates anyway, since MIT and many other licenses require it. What about recording the compiler version? The compiler itself will start embedding it soon. On older versions it's already there in the debug info. On Unix you can run strings your_executable | grep 'rustc version' to see it. What about keeping track of versions of statically linked C libraries? Good question. I don't think they are exposed in any reasonable way right now. Would be a great addition, but not required for the initial launch. We can add it later in a backwards\-compatible way. Adopting the \-src crate convention would make it happen naturally, and will have other benefits as well, so that's probably the best route. What is blocking uplifting this into Cargo? Cargo itself is currently in a feature freeze. .SH EXIT STATUS .TP \fB0\fR Successful program execution. .TP \fB1\fR Unsuccessful program execution. .TP \fB101\fR The program panicked. .SH EXAMPLES .TP Build your project with dependency lists embedded in the binaries \fB# cargo auditable build \-\-release\fR .SH AUTHOR .P .RS 2 .nf Sergey "Shnatsel" Davidoff cargo-auditable-0.6.6/src/auditable_from_metadata.rs000064400000000000000000000240661046102023000206420ustar 00000000000000//! Converts from `cargo_metadata` crate structs to `auditable-serde` structs, //! which map to our own serialialized representation. use std::{cmp::min, cmp::Ordering::*, collections::HashMap, error::Error, fmt::Display}; use auditable_serde::{DependencyKind, Package, Source, VersionInfo}; fn source_from_meta(meta_source: &cargo_metadata::Source) -> Source { match meta_source.repr.as_str() { "registry+https://github.com/rust-lang/crates.io-index" => Source::CratesIo, source => Source::from( source .split('+') .next() .expect("Encoding of source strings in `cargo metadata` has changed!"), ), } } /// The values are ordered from weakest to strongest so that casting to integer would make sense #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] enum PrivateDepKind { Development, Build, Runtime, } impl From for DependencyKind { fn from(priv_kind: PrivateDepKind) -> Self { match priv_kind { PrivateDepKind::Development => { panic!("Cannot convert development dependency to serializable format") } PrivateDepKind::Build => DependencyKind::Build, PrivateDepKind::Runtime => DependencyKind::Runtime, } } } impl From<&cargo_metadata::DependencyKind> for PrivateDepKind { fn from(kind: &cargo_metadata::DependencyKind) -> Self { match kind { cargo_metadata::DependencyKind::Normal => PrivateDepKind::Runtime, cargo_metadata::DependencyKind::Development => PrivateDepKind::Development, cargo_metadata::DependencyKind::Build => PrivateDepKind::Build, _ => panic!("Unknown dependency kind"), } } } /// Error returned by the conversion from /// [`cargo_metadata::Metadata`](https://docs.rs/cargo_metadata/0.11.1/cargo_metadata/struct.Metadata.html) #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum InsufficientMetadata { NoDeps, VirtualWorkspace, } impl Display for InsufficientMetadata { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InsufficientMetadata::NoDeps => { write!(f, "Missing dependency information! Please call 'cargo metadata' without '--no-deps' flag.") } InsufficientMetadata::VirtualWorkspace => { write!(f, "Missing root crate! Please call this from a package directory, not workspace root.") } } } } impl Error for InsufficientMetadata {} pub fn encode_audit_data( metadata: &cargo_metadata::Metadata, ) -> Result { let toplevel_crate_id = metadata .resolve .as_ref() .ok_or(InsufficientMetadata::NoDeps)? .root .as_ref() .ok_or(InsufficientMetadata::VirtualWorkspace)? .repr .as_str(); // Walk the dependency tree and resolve dependency kinds for each package. // We need this because there may be several different paths to the same package // and we need to aggregate dependency types across all of them. // Moreover, `cargo metadata` doesn't propagate dependency information: // A runtime dependency of a build dependency of your package should be recorded // as *build* dependency, but Cargo flags it as a runtime dependency. // Hoo boy, here I go hand-rolling BFS again! let nodes = &metadata.resolve.as_ref().unwrap().nodes; let id_to_node: HashMap<&str, &cargo_metadata::Node> = nodes.iter().map(|n| (n.id.repr.as_str(), n)).collect(); let mut id_to_dep_kind: HashMap<&str, PrivateDepKind> = HashMap::new(); id_to_dep_kind.insert(toplevel_crate_id, PrivateDepKind::Runtime); let mut current_queue: Vec<&cargo_metadata::Node> = vec![id_to_node[toplevel_crate_id]]; let mut next_step_queue: Vec<&cargo_metadata::Node> = Vec::new(); while !current_queue.is_empty() { for parent in current_queue.drain(..) { let parent_dep_kind = id_to_dep_kind[parent.id.repr.as_str()]; for child in &parent.deps { let child_id = child.pkg.repr.as_str(); let dep_kind = strongest_dep_kind(child.dep_kinds.as_slice()); let dep_kind = min(dep_kind, parent_dep_kind); let dep_kind_on_previous_visit = id_to_dep_kind.get(child_id); if dep_kind_on_previous_visit.is_none() || &dep_kind > dep_kind_on_previous_visit.unwrap() { // if we haven't visited this node in dependency graph yet // or if we've visited it with a weaker dependency type, // records its new dependency type and add it to the queue to visit its dependencies id_to_dep_kind.insert(child_id, dep_kind); next_step_queue.push(id_to_node[child_id]); } } } std::mem::swap(&mut next_step_queue, &mut current_queue); } let metadata_package_dep_kind = |p: &cargo_metadata::Package| { let package_id = p.id.repr.as_str(); id_to_dep_kind.get(package_id) }; // Remove dev-only dependencies from the package list and collect them to Vec let mut packages: Vec<&cargo_metadata::Package> = metadata .packages .iter() .filter(|p| { let dep_kind = metadata_package_dep_kind(p); // Dependencies that are present in the workspace but not used by the current root crate // will not be in the map we've built by traversing the root crate's dependencies. // In this case they will not be in the map at all. We skip them, along with dev-dependencies. dep_kind.is_some() && dep_kind.unwrap() != &PrivateDepKind::Development }) .collect(); // This function is the simplest place to introduce sorting, since // it contains enough data to distinguish between equal-looking packages // and provide a stable sorting that might not be possible // using the data from VersionInfo struct alone. // // We use sort_unstable here because there is no point in // not reordering equal elements, since they're supplied by // in arbitrary order by cargo-metadata anyway // and the order even varies between executions. packages.sort_unstable_by(|a, b| { // This is a workaround for Package not implementing Ord. // Deriving it in cargo_metadata might be more reliable? let names_order = a.name.cmp(&b.name); if names_order != Equal { return names_order; } let versions_order = a.name.cmp(&b.name); if versions_order != Equal { return versions_order; } // IDs are unique so comparing them should be sufficient a.id.repr.cmp(&b.id.repr) }); // Build a mapping from package ID to the index of that package in the Vec // because serializable representation doesn't store IDs let mut id_to_index = HashMap::new(); for (index, package) in packages.iter().enumerate() { id_to_index.insert(package.id.repr.as_str(), index); } // Convert packages from cargo-metadata representation to our representation let mut packages: Vec = packages .into_iter() .map(|p| Package { name: p.name.to_owned(), version: p.version.clone(), source: p.source.as_ref().map_or(Source::Local, source_from_meta), kind: (*metadata_package_dep_kind(p).unwrap()).into(), dependencies: Vec::new(), root: p.id.repr == toplevel_crate_id, }) .collect(); // Fill in dependency info from resolved dependency graph for node in metadata.resolve.as_ref().unwrap().nodes.iter() { let package_id = node.id.repr.as_str(); if id_to_index.contains_key(package_id) { // dev-dependencies are not included let package: &mut Package = &mut packages[id_to_index[package_id]]; // Dependencies for dep in node.deps.iter() { // Omit the graph edge if this is a development dependency // to fix https://github.com/rustsec/rustsec/issues/1043 // It is possible that something that we depend on normally // is also a dev-dependency for something, // and dev-dependencies are allowed to have cycles, // so we may end up encoding cyclic graph if we don't handle that. let dep_id = dep.pkg.repr.as_str(); if strongest_dep_kind(&dep.dep_kinds) != PrivateDepKind::Development { package.dependencies.push(id_to_index[dep_id]); } } // .sort_unstable() is fine because they're all integers package.dependencies.sort_unstable(); } } Ok(VersionInfo { packages }) } fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind { deps.iter() .map(|d| PrivateDepKind::from(&d.kind)) .max() .unwrap_or(PrivateDepKind::Runtime) // for compatibility with Rust earlier than 1.41 } #[cfg(test)] mod tests { #![allow(unused_imports)] // otherwise conditional compilation emits warnings use super::*; use std::fs; use std::{ convert::TryInto, path::{Path, PathBuf}, str::FromStr, }; fn load_metadata(cargo_toml_path: &Path) -> cargo_metadata::Metadata { let mut cmd = cargo_metadata::MetadataCommand::new(); cmd.manifest_path(cargo_toml_path); cmd.exec().unwrap() } #[test] fn dependency_cycle() { let cargo_toml_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) .join("tests/fixtures/cargo-audit-dep-cycle/Cargo.toml"); let metadata = load_metadata(&cargo_toml_path); let version_info_struct: VersionInfo = encode_audit_data(&metadata).unwrap(); let json = serde_json::to_string(&version_info_struct).unwrap(); VersionInfo::from_str(&json).unwrap(); // <- the part we care about succeeding } } cargo-auditable-0.6.6/src/binary_file.rs000064400000000000000000000027771046102023000163150ustar 00000000000000//! Wrapper around object_file.rs to keep it as intact as possible, //! because it is lifted from rustc use crate::{object_file, platform_detection::is_wasm, target_info::RustcTargetInfo}; /// Creates a binary file (ELF/Mach-O/PE/WASM) with the specified contents in a given section /// which can be passed to the linker to include the section into the final executable. /// /// Returns `None` if the architecture is not supported. pub fn create_binary_file( target_info: &RustcTargetInfo, target_triple: &str, contents: &[u8], symbol_name: &str, ) -> Option> { if is_wasm(target_info) { Some(create_wasm_file(target_info, contents)) } else { object_file::create_metadata_file(target_info, target_triple, contents, symbol_name) } } pub fn create_wasm_file( // formerly `create_compressed_metadata_file` in the rustc codebase target_info: &RustcTargetInfo, contents: &[u8], ) -> Vec { assert!(is_wasm(target_info)); // Start with the minimum valid WASM file let mut result: Vec = vec![0, b'a', b's', b'm', 1, 0, 0, 0]; // Add the `linking` section with version 2 that rust-lld expects. // This is required to mark the WASM file as relocatable, // otherwise the linker will reject it as a non-linkable file. // https://github.com/WebAssembly/tool-conventions/blob/master/Linking.md wasm_gen::write_custom_section(&mut result, "linking", &[2]); wasm_gen::write_custom_section(&mut result, ".dep-v0", contents); result } cargo-auditable-0.6.6/src/cargo_arguments.rs000064400000000000000000000066731046102023000172110ustar 00000000000000use std::ffi::OsString; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] /// Includes only the cargo arguments we care about pub struct CargoArgs { pub offline: bool, pub locked: bool, pub frozen: bool, pub config: Vec, } impl CargoArgs { /// Extracts Cargo flags from the arguments to the current process pub fn from_args() -> CargoArgs { // we .skip(3) to get over `cargo auditable build` and to the start of the flags let raw_args: Vec = std::env::args_os().skip(3).collect(); Self::from_args_vec(raw_args) } /// Split into its own function for unit testing fn from_args_vec(mut raw_args: Vec) -> CargoArgs { // if there is a -- in the invocation somewhere, only parse up to it if let Some(position) = raw_args.iter().position(|s| s == "--") { raw_args.truncate(position); } let mut parser = pico_args::Arguments::from_vec(raw_args); CargoArgs { config: parser.values_from_str("--config").unwrap(), offline: parser.contains("--offline"), locked: parser.contains("--locked"), frozen: parser.contains("--frozen"), } } /// Recovers `SerializedCargoArgs` from an environment variable (if it was exported earlier) pub fn from_env() -> Result { let json_args = std::env::var("CARGO_AUDITABLE_ORIG_ARGS")?; // We unwrap here because we've serialized these args ourselves and they should roundtrip cleanly. // Deserialization would only fail if someone tampered with them in transit. Ok(serde_json::from_str(&json_args).unwrap()) } } #[cfg(test)] mod tests { use super::*; #[test] fn basic_parsing() { let input = [ "cargo", "auditable", "build", "--locked", "--config", "net.git-fetch-with-cli=true", "--offline", ]; let raw_args = input.iter().map(OsString::from).collect(); let args = CargoArgs::from_args_vec(raw_args); assert!(args.locked); assert!(args.offline); assert!(!args.frozen); assert_eq!(args.config, vec!["net.git-fetch-with-cli=true"]); } #[test] fn with_unrelated_flags() { let input = [ "cargo", "auditable", "build", "--locked", "--target", "x86_64-unknown-linux-gnu", "--release", "--config", "net.git-fetch-with-cli=true", "--offline", "--ignore-rust-version", ]; let raw_args = input.iter().map(OsString::from).collect(); let args = CargoArgs::from_args_vec(raw_args); assert!(args.locked); assert!(args.offline); assert!(!args.frozen); assert_eq!(args.config, vec!["net.git-fetch-with-cli=true"]); } #[test] fn double_dash_to_ignore_args() { let input = [ "cargo", "auditable", "run", "--release", "--config", "net.git-fetch-with-cli=true", "--", "--offline", ]; let raw_args = input.iter().map(OsString::from).collect(); let args = CargoArgs::from_args_vec(raw_args); assert!(!args.offline); assert_eq!(args.config, vec!["net.git-fetch-with-cli=true"]); } } cargo-auditable-0.6.6/src/cargo_auditable.rs000064400000000000000000000053151046102023000171260ustar 00000000000000use crate::cargo_arguments::CargoArgs; use std::{env, process::Command}; pub fn main() { // set the RUSTFLAGS environment variable to inject our object and call Cargo with all the Cargo args // Cargo sets the path to itself in the `CARGO` environment variable: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-3rd-party-subcommands // This is also useful for using `cargo auditable` as a drop-in replacement for Cargo. let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); let mut command = Command::new(cargo); // Pass along all our arguments; we don't currently have any args specific to `cargo auditable` // We skip argv[0] which is the path to this binary and the first argument which is 'auditable' passed by Cargo command.args(env::args_os().skip(2)); // Set the environment variable to use this binary as a rustc wrapper, that's when we do the real work // It's important that we set RUSTC_WORKSPACE_WRAPPER and not RUSTC_WRAPPER because only the former invalidates cache. // If we use RUSTC_WRAPPER, running `cargo auditable` will not trigger a rebuild. // The WORKSPACE part is a bit of a misnomer: it will be run for a local crate even if there's just one, not a workspace. // Security note: // `std::env::current_exe()` is not supposed to be relied on for security - the binary may be moved, etc. // But should not a code execution vulnerability since whoever sets this could set RUSTC_WORKSPACE_WRAPPER themselves // This would matter if the binary was made setuid, but it isn't, so this should be fine. let path_to_this_binary = std::env::current_exe().unwrap(); command.env("RUSTC_WORKSPACE_WRAPPER", path_to_this_binary); // Pass on the arguments we received so that they can be inspected later. // We're interested in flags like `--offline` and `--config` which have to be passed to `cargo metadata` later. // The shell has already split them for us and we don't want to mangle them, but we need to round-trip them // through a string. Since we already depend on `serde-json` and it does the job, use JSON. // This doesn't support non-UTF8 arguments, but `cargo_metadata` crate doesn't support them either, // so this is not an issue right now. // If it ever becomes one, we could use the `serde-bytes-repr` crate for a clean round-trip. let args = CargoArgs::from_args(); let args_in_json = serde_json::to_string(&args).unwrap(); command.env("CARGO_AUDITABLE_ORIG_ARGS", args_in_json); let results = command .status() .expect("Failed to invoke cargo! Make sure it's in your $PATH"); std::process::exit(results.code().unwrap()); } cargo-auditable-0.6.6/src/collect_audit_data.rs000064400000000000000000000101521046102023000176200ustar 00000000000000use cargo_metadata::{Metadata, MetadataCommand}; use miniz_oxide::deflate::compress_to_vec_zlib; use std::str::from_utf8; use crate::{ auditable_from_metadata::encode_audit_data, cargo_arguments::CargoArgs, rustc_arguments::RustcArgs, }; /// Calls `cargo metadata` to obtain the dependency tree, serializes it to JSON and compresses it pub fn compressed_dependency_list(rustc_args: &RustcArgs, target_triple: &str) -> Vec { let metadata = get_metadata(rustc_args, target_triple); let version_info = encode_audit_data(&metadata).unwrap(); let json = serde_json::to_string(&version_info).unwrap(); // compression level 7 makes this complete in a few milliseconds, so no need to drop to a lower level in debug mode let compressed_json = compress_to_vec_zlib(json.as_bytes(), 7); compressed_json } fn get_metadata(args: &RustcArgs, target_triple: &str) -> Metadata { let mut metadata_command = MetadataCommand::new(); // Cargo sets the path to itself in the `CARGO` environment variable: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-3rd-party-subcommands // This is also useful for using `cargo auditable` as a drop-in replacement for Cargo. if let Some(path) = std::env::var_os("CARGO") { metadata_command.cargo_path(path); } // Point cargo-metadata to the correct Cargo.toml in a workspace. // CARGO_MANIFEST_DIR env var will be set by Cargo when it calls our rustc wrapper let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap(); metadata_command.current_dir(manifest_dir); // Pass the features that are actually enabled for this crate to cargo-metadata let mut features = args.enabled_features(); if let Some(index) = features.iter().position(|x| x == &"default") { features.remove(index); } else { metadata_command.features(cargo_metadata::CargoOpt::NoDefaultFeatures); } let owned_features: Vec = features.iter().map(|s| s.to_string()).collect(); metadata_command.features(cargo_metadata::CargoOpt::SomeFeatures(owned_features)); // Restrict the dependency resolution to just the platform the binary is being compiled for. // By default `cargo metadata` resolves the dependency tree for all platforms. let mut other_args = vec!["--filter-platform".to_owned(), target_triple.to_owned()]; // Pass arguments such as `--config`, `--offline` and `--locked` // from the original CLI invocation of `cargo auditable` let orig_args = CargoArgs::from_env() .expect("Env var 'CARGO_AUDITABLE_ORIG_ARGS' set by 'cargo-auditable' is unset!"); if orig_args.offline { other_args.push("--offline".to_owned()); } if orig_args.frozen { other_args.push("--frozen".to_owned()); } if orig_args.locked { other_args.push("--locked".to_owned()); } for arg in orig_args.config { other_args.push("--config".to_owned()); other_args.push(arg); } // This can only be done once, multiple calls will replace previously set options. metadata_command.other_options(other_args); // Get the underlying std::process::Command and re-implement MetadataCommand::exec, // to clear RUSTC_WORKSPACE_WRAPPER in the child process to avoid recursion. // The alternative would be modifying the environment of our own process, // which is sketchy and discouraged on POSIX because it's not thread-safe: // https://doc.rust-lang.org/stable/std/env/fn.remove_var.html let mut metadata_command = metadata_command.cargo_command(); metadata_command.env_remove("RUSTC_WORKSPACE_WRAPPER"); let output = metadata_command.output().unwrap(); if !output.status.success() { panic!( "cargo metadata failure: {}", String::from_utf8_lossy(&output.stderr) ); } let stdout = from_utf8(&output.stdout) .expect("cargo metadata output not utf8") .lines() .find(|line| line.starts_with('{')) .expect("cargo metadata output not json"); MetadataCommand::parse(stdout).expect("failed to parse cargo metadata output") } cargo-auditable-0.6.6/src/main.rs000064400000000000000000000031141046102023000147400ustar 00000000000000#![forbid(unsafe_code)] mod auditable_from_metadata; mod binary_file; mod cargo_arguments; mod cargo_auditable; mod collect_audit_data; mod object_file; mod platform_detection; mod rustc_arguments; mod rustc_wrapper; mod target_info; use std::process::exit; /// Dispatches the call to either `cargo auditable` when invoked through cargo, /// or to `rustc_wrapper` when Cargo internals invoke it fn main() { let first_arg = std::env::args_os().nth(1); if let Some(arg) = first_arg { if arg == "auditable" { cargo_auditable::main() } // When this binary is called as a rustc wrapper, the first argument is the path to rustc: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads // It's important to read it because it can be overridden via env vars or config files. // In order to distinguish that from someone running the binary directly by mistake, // we check if the env var we set earlier is still present. // The "rustc" special-case is purely to accommodate the weird things `sccache` does: // https://github.com/rust-secure-code/cargo-auditable/issues/87 // We should push back and make it sccache's problem if this ever causes issues. else if arg == "rustc" || std::env::var_os("CARGO_AUDITABLE_ORIG_ARGS").is_some() { rustc_wrapper::main(&arg) } else { shoo(); } } else { shoo(); } } fn shoo() -> ! { eprintln!("'cargo auditable' should be invoked through Cargo"); exit(1); } cargo-auditable-0.6.6/src/object_file.rs000064400000000000000000000345301046102023000162670ustar 00000000000000//! Shamelessly copied from rustc codebase: //! https://github.com/rust-lang/rust/blob/dcca6a375bd4eddb3deea7038ebf29d02af53b48/compiler/rustc_codegen_ssa/src/back/metadata.rs#L97-L206 //! and butchered ever so slightly use object::write::{self, StandardSegment, Symbol, SymbolSection}; use object::{ elf, Architecture, BinaryFormat, Endianness, FileFlags, SectionFlags, SectionKind, SymbolFlags, SymbolKind, SymbolScope, }; use crate::platform_detection::{is_32bit, is_apple, is_windows}; use crate::target_info::RustcTargetInfo; /// Returns None if the architecture is not supported pub fn create_metadata_file( // formerly `create_compressed_metadata_file` in the rustc codebase target_info: &RustcTargetInfo, target_triple: &str, contents: &[u8], symbol_name: &str, ) -> Option> { let mut file = create_object_file(target_info, target_triple)?; let section = file.add_section( file.segment_name(StandardSegment::Data).to_vec(), b".dep-v0".to_vec(), SectionKind::ReadOnlyData, ); if let BinaryFormat::Elf = file.format() { // Explicitly set no flags to avoid SHF_ALLOC default for data section. file.section_mut(section).flags = SectionFlags::Elf { sh_flags: 0 }; }; let offset = file.append_section_data(section, contents, 1); // For MachO and probably PE this is necessary to prevent the linker from throwing away the // .rustc section. For ELF this isn't necessary, but it also doesn't harm. file.add_symbol(Symbol { name: symbol_name.as_bytes().to_vec(), value: offset, size: contents.len() as u64, kind: SymbolKind::Data, scope: SymbolScope::Dynamic, weak: false, section: SymbolSection::Section(section), flags: SymbolFlags::None, }); Some(file.write().unwrap()) } fn create_object_file( info: &RustcTargetInfo, target_triple: &str, ) -> Option> { // This conversion evolves over time, and has some subtle logic for MIPS and RISC-V later on, that also evolves. // If/when uplifiting this into Cargo, we will need to extract this code from rustc and put it in the `object` crate // so that it could be shared between rustc and Cargo. let endianness = match info["target_endian"].as_str() { "little" => Endianness::Little, "big" => Endianness::Big, _ => unreachable!(), }; let architecture = match info["target_arch"].as_str() { "arm" => Architecture::Arm, "aarch64" => { if is_32bit(info) { Architecture::Aarch64_Ilp32 } else { Architecture::Aarch64 } } "x86" => Architecture::I386, "s390x" => Architecture::S390x, "mips" => Architecture::Mips, "mips64" => Architecture::Mips64, "x86_64" => { if is_32bit(info) { Architecture::X86_64_X32 } else { Architecture::X86_64 } } "powerpc" => Architecture::PowerPc, "powerpc64" => Architecture::PowerPc64, "riscv32" => Architecture::Riscv32, "riscv64" => Architecture::Riscv64, "sparc64" => Architecture::Sparc64, "loongarch64" => Architecture::LoongArch64, // Unsupported architecture. _ => return None, }; let binary_format = if is_apple(info) { BinaryFormat::MachO } else if is_windows(info) { BinaryFormat::Coff } else { BinaryFormat::Elf }; let mut file = write::Object::new(binary_format, architecture, endianness); let e_flags = match architecture { Architecture::Mips => { // the original code matches on info we don't have to support pre-1999 MIPS variants: // https://github.com/rust-lang/rust/blob/dcca6a375bd4eddb3deea7038ebf29d02af53b48/compiler/rustc_codegen_ssa/src/back/metadata.rs#L144C3-L153 // We can't support them, so this part was was modified significantly. let arch = if target_triple.contains("r6") { elf::EF_MIPS_ARCH_32R6 } else { elf::EF_MIPS_ARCH_32R2 }; // end of modified part // The only ABI LLVM supports for 32-bit MIPS CPUs is o32. let mut e_flags = elf::EF_MIPS_CPIC | elf::EF_MIPS_ABI_O32 | arch; // commented out: insufficient info to support this outside rustc // if sess.target.options.relocation_model != RelocModel::Static { // e_flags |= elf::EF_MIPS_PIC; // } if target_triple.contains("r6") { e_flags |= elf::EF_MIPS_NAN2008; } e_flags } Architecture::Mips64 => { // copied from `mips64el-linux-gnuabi64-gcc foo.c -c` #[allow(clippy::let_and_return)] // for staying as close to upstream as possible let e_flags = elf::EF_MIPS_CPIC | elf::EF_MIPS_PIC | if target_triple.contains("r6") { elf::EF_MIPS_ARCH_64R6 | elf::EF_MIPS_NAN2008 } else { elf::EF_MIPS_ARCH_64R2 }; e_flags } Architecture::Riscv32 | Architecture::Riscv64 => { // Source: https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/079772828bd10933d34121117a222b4cc0ee2200/riscv-elf.adoc let mut e_flags: u32 = 0x0; let features = riscv_features(target_triple, info); // Check if compressed is enabled if features.contains('c') { e_flags |= elf::EF_RISCV_RVC; } // Select the appropriate floating-point ABI if features.contains('d') { e_flags |= elf::EF_RISCV_FLOAT_ABI_DOUBLE; } else if features.contains('f') { e_flags |= elf::EF_RISCV_FLOAT_ABI_SINGLE; } else { e_flags |= elf::EF_RISCV_FLOAT_ABI_SOFT; } e_flags } Architecture::LoongArch64 => { // Source: https://github.com/loongson/la-abi-specs/blob/release/laelf.adoc#e_flags-identifies-abi-type-and-version let mut e_flags: u32 = elf::EF_LARCH_OBJABI_V1; let features = loongarch_features(target_triple); // Select the appropriate floating-point ABI if features.contains('d') { e_flags |= elf::EF_LARCH_ABI_DOUBLE_FLOAT; } else if features.contains('f') { e_flags |= elf::EF_LARCH_ABI_SINGLE_FLOAT; } else { e_flags |= elf::EF_LARCH_ABI_SOFT_FLOAT; } e_flags } _ => 0, }; // adapted from LLVM's `MCELFObjectTargetWriter::getOSABI` let os_abi = match info["target_os"].as_str() { "hermit" => elf::ELFOSABI_STANDALONE, "freebsd" => elf::ELFOSABI_FREEBSD, "solaris" => elf::ELFOSABI_SOLARIS, _ => elf::ELFOSABI_NONE, }; let abi_version = 0; file.flags = FileFlags::Elf { os_abi, abi_version, e_flags, }; Some(file) } // This function was not present in the original rustc code, which simply used // `sess.target.options.features` // We do not have access to compiler internals, so we have to reimplement this function. // And `rustc --print=cfg` doesn't expose some of the features we care about, // specifically the 'd' and 'f' features. // Hence this function, which is not as robust as I would like. fn riscv_features(target_triple: &str, info: &RustcTargetInfo) -> String { let arch = target_triple.split('-').next().unwrap(); assert_eq!(&arch[..5], "riscv"); let mut extensions = arch[7..].to_owned(); if extensions.contains('g') { extensions.push_str("imadf"); } // Most but not all riscv targets declare target features. // A notable exception is `riscv64-linux-android`. // We assume that all Linux-capable targets are -gc. match info["target_os"].as_str() { "linux" | "android" => extensions.push_str("imadfc"), _ => (), } extensions } // This function was not present in the original rustc code, which simply used // `sess.target.options.features` // We do not have access to compiler internals, so we have to reimplement this function. fn loongarch_features(target_triple: &str) -> String { match target_triple { "loongarch64-unknown-none-softfloat" => "".to_string(), _ => "f,d".to_string(), } } #[cfg(test)] mod tests { use std::collections::HashMap; use super::*; use crate::target_info::parse_rustc_target_info; #[test] fn test_riscv_abi_detection() { // real-world target with double floats let info = HashMap::from([("target_os".to_owned(), "linux".to_owned())]); let features = riscv_features("riscv64gc-unknown-linux-gnu", &info); assert!(features.contains('c')); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target without floats let info = HashMap::from([("target_os".to_owned(), "none".to_owned())]); let features = riscv_features("riscv32imac-unknown-none-elf", &info); assert!(features.contains('c')); assert!(!features.contains('d')); assert!(!features.contains('f')); // real-world target without floats or compression let info = HashMap::from([("target_os".to_owned(), "none".to_owned())]); let features = riscv_features("riscv32i-unknown-none-elf", &info); assert!(!features.contains('c')); assert!(!features.contains('d')); assert!(!features.contains('f')); // made-up target without compression and with single floats let info = HashMap::from([("target_os".to_owned(), "none".to_owned())]); let features = riscv_features("riscv32if-unknown-none-elf", &info); assert!(!features.contains('c')); assert!(!features.contains('d')); assert!(features.contains('f')); // real-world Android riscv target let info = HashMap::from([("target_os".to_owned(), "android".to_owned())]); let features = riscv_features("riscv64-linux-android", &info); assert!(features.contains('c')); assert!(features.contains('d')); assert!(features.contains('f')); } #[test] fn test_loongarch_abi_detection() { // real-world target with double floats let features = loongarch_features("loongarch64-unknown-linux-gnu"); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target with double floats let features = loongarch_features("loongarch64-unknown-linux-musl"); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target with double floats let features = loongarch_features("loongarch64-unknown-none"); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target with soft floats let features = loongarch_features("loongarch64-unknown-none-softfloat"); assert!(!features.contains('d')); assert!(!features.contains('f')); } #[test] fn test_create_object_file_linux() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="gnu" target_family="unix" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="linux" target_pointer_width="64" target_vendor="unknown" unix "#; let target_triple = "x86_64-unknown-linux-gnu"; let target_info = parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Elf); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_windows_msvc() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="msvc" target_family="windows" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="windows" target_pointer_width="64" target_vendor="pc" windows "#; let target_triple = "x86_64-pc-windows-msvc"; let target_info = parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Coff); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_windows_gnu() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="gnu" target_family="windows" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="windows" target_pointer_width="64" target_vendor="pc" windows "#; let target_triple = "x86_64-pc-windows-gnu"; let target_info = crate::target_info::parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Coff); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_macos() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="" target_family="unix" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_feature="sse3" target_feature="ssse3" target_os="macos" target_pointer_width="64" target_vendor="apple" unix "#; let target_triple = "x86_64-apple-darwin"; let target_info = crate::target_info::parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::MachO); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_linux_arm() { let rustc_output = br#"debug_assertions target_arch="aarch64" target_endian="little" target_env="gnu" target_family="unix" target_os="linux" target_pointer_width="64" target_vendor="unknown" unix "#; let target_triple = "aarch64-unknown-linux-gnu"; let target_info = parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Elf); assert_eq!(result.architecture(), Architecture::Aarch64); } } cargo-auditable-0.6.6/src/platform_detection.rs000064400000000000000000000014631046102023000177030ustar 00000000000000//! Utilities to reliably and consistently detect various platforms use crate::target_info::RustcTargetInfo; pub fn is_wasm(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_family", "wasm") } pub fn is_msvc(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_env", "msvc") } pub fn is_apple(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_vendor", "apple") } pub fn is_windows(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_os", "windows") } pub fn is_32bit(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_pointer_width", "32") } fn key_equals(target_info: &RustcTargetInfo, key: &str, value: &str) -> bool { target_info.get(key).map(|s| s.as_str()) == Some(value) } cargo-auditable-0.6.6/src/rustc_arguments.rs000064400000000000000000000147161046102023000172530ustar 00000000000000//! Parses rustc arguments to extract the info not provided via environment variables. use std::{ffi::OsString, path::PathBuf}; // We use pico-args because we only need to extract a few specific arguments out of a larger set, // and other parsers (rustc's `getopts`, cargo's `clap`) make that difficult. // // We also intentionally do very little validation, to avoid rejecting new configurations // that may be added to rustc in the future. // // For reference, the rustc argument parsing code is at // https://github.com/rust-lang/rust/blob/26ecd44160f54395b3bd5558cc5352f49cb0a0ba/compiler/rustc_session/src/config.rs /// Includes only the rustc arguments we care about #[derive(Debug)] pub struct RustcArgs { pub crate_name: String, pub crate_types: Vec, pub cfg: Vec, pub emit: Vec, pub out_dir: PathBuf, pub target: Option, pub print: Vec, } impl RustcArgs { pub fn enabled_features(&self) -> Vec<&str> { let mut result = Vec::new(); for item in &self.cfg { if item.starts_with("feature=\"") { // feature names cannot contain quotes according to the documentation: // https://doc.rust-lang.org/cargo/reference/features.html#the-features-section result.push(item.split('"').nth(1).unwrap()); } } result } } impl RustcArgs { // Split into its own function for unit testing fn from_vec(raw_args: Vec) -> Result { let mut parser = pico_args::Arguments::from_vec(raw_args); // --emit requires slightly more complex parsing let raw_emit_args: Vec = parser.values_from_str("--emit")?; let mut emit: Vec = Vec::new(); for raw_arg in raw_emit_args { for item in raw_arg.split(',') { emit.push(item.to_owned()); } } Ok(RustcArgs { crate_name: parser.value_from_str("--crate-name")?, crate_types: parser.values_from_str("--crate-type")?, cfg: parser.values_from_str("--cfg")?, emit, out_dir: parser .value_from_os_str::<&str, PathBuf, pico_args::Error>("--out-dir", |s| { Ok(PathBuf::from(s)) })?, target: parser.opt_value_from_str("--target")?, print: parser.values_from_str("--print")?, }) } } pub fn parse_args() -> Result { let raw_args: Vec = std::env::args_os().skip(2).collect(); RustcArgs::from_vec(raw_args) } pub fn should_embed_audit_data(args: &RustcArgs) -> bool { // Only inject audit data into crate types 'bin' and 'cdylib', // it doesn't make sense for static libs and weird other types. if !(args.crate_types.contains(&"bin".to_owned()) || args.crate_types.contains(&"cdylib".to_owned())) { return false; } // when --emit is specified explicitly, only inject audit data for --emit=link // because it doesn't make sense for all other types such as llvm-ir, asm, etc. if !args.emit.is_empty() && !args.emit.contains(&"link".to_owned()) { return false; } // --print disables compilation UNLESS --emit is also specified if !args.print.is_empty() && args.emit.is_empty() { return false; } true } #[cfg(test)] mod tests { use super::*; #[test] fn cargo_c_compatibility() { let raw_rustc_args = vec!["--crate-name", "rustls", "--edition=2021", "src/lib.rs", "--error-format=json", "--json=diagnostic-rendered-ansi,artifacts,future-incompat", "--crate-type", "staticlib", "--crate-type", "cdylib", "--emit=dep-info,link", "-C", "embed-bitcode=no", "-C", "debuginfo=2", "-C", "link-arg=-Wl,-soname,librustls.so.0.14.0", "-Cmetadata=rustls-ffi", "--cfg", "cargo_c", "--print", "native-static-libs", "--cfg", "feature=\"aws-lc-rs\"", "--cfg", "feature=\"capi\"", "--cfg", "feature=\"default\"", "--check-cfg", "cfg(docsrs)", "--check-cfg", "cfg(feature, values(\"aws-lc-rs\", \"capi\", \"cert_compression\", \"default\", \"no_log_capture\", \"read_buf\", \"ring\"))", "-C", "metadata=b6a43041f637feb8", "--out-dir", "/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps", "--target", "x86_64-unknown-linux-gnu", "-C", "linker=clang", "-C", "incremental=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/incremental", "-L", "dependency=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps", "-L", "dependency=/home/user/Code/rustls-ffi/target/debug/deps", "--extern", "libc=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/liblibc-4fc7c9f82dda33ee.rlib", "--extern", "log=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/liblog-6f7c8f4d1d5ec422.rlib", "--extern", "rustls=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls-a93cda0ba0380929.rlib", "--extern", "pki_types=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls_pki_types-27749859644f0979.rlib", "--extern", "rustls_platform_verifier=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls_platform_verifier-bceca5cf09f3d7ba.rlib", "--extern", "webpki=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/libwebpki-bc4a16dd84e0b062.rlib", "-C", "link-arg=-fuse-ld=/home/user/mold-2.32.0-x86_64-linux/bin/mold", "-L", "native=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/build/aws-lc-sys-d52f8990d9ede41d/out"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); assert!(should_embed_audit_data(&args)); } #[test] fn multiple_emit_values() { let raw_rustc_args = vec![ "--emit=dep-info,link", "--emit", "llvm-bc", // end of interesting args, start of boilerplate "--crate-name", "foobar", "--out-dir", "/foo/bar", ]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let mut args = RustcArgs::from_vec(raw_rustc_args).unwrap(); let expected = vec!["dep-info", "link", "llvm-bc"]; let mut expected: Vec = expected.into_iter().map(|s| s.into()).collect(); args.emit.sort(); expected.sort(); assert_eq!(args.emit, expected) } } cargo-auditable-0.6.6/src/rustc_wrapper.rs000064400000000000000000000133701046102023000167210ustar 00000000000000use std::{ env, ffi::{OsStr, OsString}, process::Command, }; use crate::{ binary_file, collect_audit_data, platform_detection::{is_apple, is_msvc, is_wasm}, rustc_arguments::{self, should_embed_audit_data}, target_info, }; use std::io::BufRead; pub fn main(rustc_path: &OsStr) { let mut command = rustc_command(rustc_path); // Binaries and C dynamic libraries are not built as non-primary packages, // so this should not cause issues with Cargo caches. if env::var_os("CARGO_PRIMARY_PACKAGE").is_some() { let arg_parsing_result = rustc_arguments::parse_args(); if let Ok(args) = rustc_arguments::parse_args() { if should_embed_audit_data(&args) { // Get the audit data to embed let target_triple = args .target .clone() .unwrap_or_else(|| rustc_host_target_triple(rustc_path)); let contents: Vec = collect_audit_data::compressed_dependency_list(&args, &target_triple); // write the audit info to an object file let target_info = target_info::rustc_target_info(rustc_path, &target_triple); let binfile = binary_file::create_binary_file( &target_info, &target_triple, &contents, "AUDITABLE_VERSION_INFO", ); if let Some(file) = binfile { // Place the audit data in the output dir. // We can place it anywhere really, the only concern is clutter and name collisions, // and the target dir is locked so we're probably good let filename = format!("{}_audit_data.o", args.crate_name); let path = args.out_dir.join(filename); std::fs::write(&path, file).expect("Unable to write output file"); // Modify the rustc command to link the object file with audit data let mut linker_command = OsString::from("-Clink-arg="); linker_command.push(&path); command.arg(linker_command); // Prevent the symbol from being removed as unused by the linker if is_apple(&target_info) { command.arg("-Clink-arg=-Wl,-u,_AUDITABLE_VERSION_INFO"); } else if is_msvc(&target_info) { command.arg("-Clink-arg=/INCLUDE:AUDITABLE_VERSION_INFO"); } else if is_wasm(&target_info) { // We don't emit the symbol name in WASM, so nothing to do } else { command.arg("-Clink-arg=-Wl,--undefined=AUDITABLE_VERSION_INFO"); } } else { // create_binary_file() returned None, indicating an unsupported architecture eprintln!("WARNING: target '{target_triple}' is not supported by 'cargo auditable'!\n\ The build will continue, but no audit data will be injected into the binary."); } } } else { // Failed to parse rustc arguments. // This may be due to a `rustc -vV` call, or similar non-compilation command. // This never happens with Cargo - it does call `rustc -vV`, // but either bypasses the wrapper or doesn't set CARGO_PRIMARY_PACKAGE=true. // However it does happen with `sccache`: // https://github.com/rust-secure-code/cargo-auditable/issues/87 // This is probably a bug in `sccache`, but it's easier to fix here. // There are many non-compilation flags (and they can be compound), // so parsing them properly adds a lot of complexity. // So we just check if `--crate-name` is passed and if not, // assume that it's a non-compilation command. if env::args_os() .skip(2) .any(|arg| arg == OsStr::new("--crate-name")) { // this was a compilation command, bail arg_parsing_result.unwrap(); } // for commands like `rustc --version` we just pass on the arguments without changes } } // Invoke rustc let results = command.status().unwrap_or_else(|err| { let mut command_with_args: Vec<&OsStr> = vec![command.get_program()]; command_with_args.extend(command.get_args()); eprintln!( "Failed to invoke rustc! Make sure it's in your $PATH\n\ The error was: {}\n\ The attempted call was: {:?}", err, command_with_args, ); std::process::exit(1); }); std::process::exit(results.code().unwrap()); } /// Creates a rustc command line and populates arguments from arguments passed to us. fn rustc_command(rustc_path: &OsStr) -> Command { let mut command = Command::new(rustc_path); // Pass along all the arguments that Cargo meant to pass to rustc // We skip the path to our binary as well as the first argument passed by Cargo, // which is the path to rustc to use (or just "rustc") command.args(env::args_os().skip(2)); command } /// Returns the default target triple for the rustc we're running fn rustc_host_target_triple(rustc_path: &OsStr) -> String { Command::new(rustc_path) .arg("-vV") .output() .expect("Failed to invoke rustc! Is it in your $PATH?") .stdout .lines() .map(|l| l.unwrap()) .find(|l| l.starts_with("host: ")) .map(|l| l[6..].to_string()) .expect("Failed to parse rustc output to determine the current platform. Please report this bug!") } cargo-auditable-0.6.6/src/target_info.rs000064400000000000000000000045131046102023000163210ustar 00000000000000use std::{ffi::OsStr, io::BufRead}; pub type RustcTargetInfo = std::collections::HashMap; pub fn rustc_target_info(rustc_path: &OsStr, target_triple: &str) -> RustcTargetInfo { // this is hand-rolled because the relevant piece of Cargo is hideously complex for some reason parse_rustc_target_info(&std::process::Command::new(rustc_path) .arg("--print=cfg") .arg(format!("--target={target_triple}")) //not being parsed by the shell, so not a vulnerability .output() .unwrap_or_else(|_| panic!("Failed to invoke rustc; make sure it's in $PATH and that '{target_triple}' is a valid target triple")) .stdout) } pub(crate) fn parse_rustc_target_info(rustc_output: &[u8]) -> RustcTargetInfo { // Decoupled from `rustc_target_info` to allow unit testing // `pub(crate)` so that unit tests in other modules could use it rustc_output .lines() .filter_map(|line| { let line = line.unwrap(); // rustc outputs some free-standing values as well as key-value pairs // we're only interested in the pairs, which are separated by '=' and the value is quoted if line.contains('=') { let key = line.split('=').next().unwrap(); let mut value: String = line.split('=').skip(1).collect(); // strip first and last chars of the quoted value. Verify that they're quotes assert!(value.pop().unwrap() == '"'); assert!(value.remove(0) == '"'); Some((key.to_owned(), value)) } else { None } }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_rustc_parser_linux() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="gnu" target_family="unix" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="linux" target_pointer_width="64" target_vendor="unknown" unix "#; let result = parse_rustc_target_info(rustc_output); assert_eq!(result.get("target_arch").unwrap(), "x86_64"); assert_eq!(result.get("target_endian").unwrap(), "little"); assert_eq!(result.get("target_pointer_width").unwrap(), "64"); assert_eq!(result.get("target_vendor").unwrap(), "unknown"); } } cargo-auditable-0.6.6/tests/it.rs000064400000000000000000000427471046102023000150220ustar 00000000000000//! Integration Tests for cargo auditable use std::{ collections::HashMap, ffi::OsStr, io::Write, path::PathBuf, process::{Command, Output, Stdio}, }; use auditable_serde::{DependencyKind, VersionInfo}; use cargo_metadata::{ camino::{Utf8Path, Utf8PathBuf}, Artifact, }; // Path to cargo-auditable binary under test const EXE: &str = env!("CARGO_BIN_EXE_cargo-auditable"); // Path to Cargo itself const CARGO: &str = env!("CARGO"); /// Run cargo auditable with --manifest-path and extra args, /// returning of map of workspace member names -> produced binaries (bin and cdylib) /// Reads the AUDITABLE_TEST_TARGET environment variable to determine the target to compile for fn run_cargo_auditable

( cargo_toml_path: P, args: &[&str], env: &[(&str, &OsStr)], ) -> HashMap> where P: AsRef, { // run `cargo clean` before performing the build, // otherwise already built binaries will be used // and we won't actually test the *current* version of `cargo auditable` let status = Command::new(CARGO) .arg("clean") .arg("--manifest-path") .arg(&cargo_toml_path) .status() .unwrap(); assert!(status.success(), "Failed to invoke `cargo clean`!"); let mut command = Command::new(EXE); command .arg("auditable") .arg("build") .arg("--release") .arg("--manifest-path") .arg(&cargo_toml_path) // We'll parse these to get binary paths .arg("--message-format=json") .args(args); if let Ok(target) = std::env::var("AUDITABLE_TEST_TARGET") { if args.iter().all(|arg| !arg.starts_with("--target")) { command.arg(format!("--target={target}")); } } for (name, value) in env { command.env(name, value); } let output = command // We don't need to read stderr, so inherit for easier test debugging .stderr(Stdio::inherit()) .stdout(Stdio::piped()) .output() .unwrap(); ensure_build_succeeded(&output); let mut bins = HashMap::new(); std::str::from_utf8(&output.stdout) .unwrap() .lines() .flat_map(|line: &str| { let mut binaries = vec![]; if let Ok(artifact) = serde_json::from_str::(line) { // workspace member name is first word in package ID let member = artifact .package_id .to_string() .split(' ') .next() .unwrap() .to_string(); // bin targets are straightforward - use executable if let Some(executable) = artifact.executable { binaries.push((member, executable)); // cdylibs less so } else if artifact .target .kind .iter() .any(|kind| kind.as_str() == "cdylib") { // Detect files with .so (Linux), .dylib (Mac) and .dll (Windows) extensions artifact .filenames .into_iter() .filter(|f| { f.extension() == Some("dylib") || f.extension() == Some("so") || f.extension() == Some("dll") }) .for_each(|f| { binaries.push((member.clone(), f)); }); } } binaries }) .for_each(|(package, binary)| { bins.entry(pkgid_to_bin_name(&package)) .or_insert(Vec::new()) .push(binary); }); bins } fn pkgid_to_bin_name(pkgid: &str) -> String { // the input is string in the format such as // "path+file:///home/shnatsel/Code/cargo-auditable/cargo-auditable/tests/fixtures/lib_and_bin_crate#0.1.0" // (for full docs see `cargo pkgid`) // and we need just the crate name, e.g. "lib_and_bin_crate". // Weirdly it doesn't use OS path separator, it always uses '/' pkgid .rsplit_once('/') .unwrap() .1 .split_once('#') .unwrap() .0 .to_owned() } fn ensure_build_succeeded(output: &Output) { if !output.status.success() { let stderr = std::io::stderr(); let mut handle = stderr.lock(); handle.write_all(&output.stdout).unwrap(); handle.write_all(&output.stderr).unwrap(); handle.flush().unwrap(); panic!("Build with `cargo auditable` failed"); } } fn get_dependency_info(binary: &Utf8Path) -> VersionInfo { auditable_info::audit_info_from_file(binary.as_std_path(), Default::default()).unwrap() } #[test] fn test_cargo_auditable_workspaces() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/workspace/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(&workspace_cargo_toml, &[], &[]); eprintln!("Test fixture binary map: {bins:?}"); // No binaries for library_crate assert!(bins.get("library_crate").is_none()); // binary_and_cdylib_crate let binary_and_cdylib_crate_bins = bins.get("binary_and_cdylib_crate").unwrap(); match std::env::var("AUDITABLE_TEST_TARGET") { // musl targets do not produce cdylibs by default: https://github.com/rust-lang/cargo/issues/8607 // So when targeting musl, we only check that the binary has been built, not the cdylib. Ok(target) if target.contains("musl") => assert!(!binary_and_cdylib_crate_bins.is_empty()), // everything else should build both the binary and cdylib _ => assert_eq!(binary_and_cdylib_crate_bins.len(), 2), } for binary in binary_and_cdylib_crate_bins { let dep_info = get_dependency_info(binary); eprintln!("{binary} dependency info: {dep_info:?}"); // binary_and_cdylib_crate should have two dependencies, library_crate and itself assert!(dep_info.packages.len() == 2); assert!(dep_info.packages.iter().any(|p| p.name == "library_crate")); assert!(dep_info .packages .iter() .any(|p| p.name == "binary_and_cdylib_crate")); } // crate_with_features should create a binary with two dependencies, library_crate and itself let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 2); assert!(dep_info.packages.iter().any(|p| p.name == "library_crate")); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_features")); // Run enabling binary_and_cdylib_crate feature let bins = run_cargo_auditable( &workspace_cargo_toml, &["--features", "binary_and_cdylib_crate"], &[], ); // crate_with_features should now have three dependencies, library_crate binary_and_cdylib_crate and crate_with_features, let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info.packages.iter().any(|p| p.name == "library_crate")); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_features")); assert!(dep_info .packages .iter() .any(|p| p.name == "binary_and_cdylib_crate")); // Run without default features let bins = run_cargo_auditable(&workspace_cargo_toml, &["--no-default-features"], &[]); // crate_with_features should now only depend on itself let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_features")); } /// This exercises a small real-world project #[test] fn test_self_hosting() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../rust-audit-info/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); eprintln!("Self-hosting binary map: {bins:?}"); // verify that the dependency info is present at all let bin = &bins.get("rust-audit-info").unwrap()[0]; let dep_info = get_dependency_info(bin); eprintln!("{bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() > 1); assert!(dep_info .packages .iter() .any(|p| p.name == "rust-audit-info")); } #[test] fn test_lto() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lto_binary_crate/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); eprintln!("LTO binary map: {bins:?}"); // lto_binary_crate should only depend on itself let lto_binary_crate_bin = &bins.get("lto_binary_crate").unwrap()[0]; let dep_info = get_dependency_info(lto_binary_crate_bin); eprintln!("{lto_binary_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "lto_binary_crate")); } #[test] fn test_bin_and_lib_in_one_crate() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lib_and_bin_crate/Cargo.toml"); let bins = run_cargo_auditable(workspace_cargo_toml, &["--bin=some_binary"], &[]); eprintln!("Test fixture binary map: {bins:?}"); // lib_and_bin_crate should only depend on itself let lib_and_bin_crate_bin = &bins.get("lib_and_bin_crate").unwrap()[0]; let dep_info = get_dependency_info(lib_and_bin_crate_bin); eprintln!("{lib_and_bin_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "lib_and_bin_crate")); } /// A previous approach had trouble with build scripts and proc macros. /// Verify that those still work. #[test] fn test_build_script() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/crate_with_build_script/Cargo.toml"); let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); eprintln!("Test fixture binary map: {bins:?}"); // crate_with_build_script should only depend on itself let crate_with_build_script_bin = &bins.get("crate_with_build_script").unwrap()[0]; let dep_info = get_dependency_info(crate_with_build_script_bin); eprintln!("{crate_with_build_script_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_build_script")); } #[test] fn test_platform_specific_deps() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/platform_specific_deps/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); eprintln!("Test fixture binary map: {bins:?}"); let test_target = std::env::var("AUDITABLE_TEST_TARGET"); if test_target.is_err() || !test_target.unwrap().starts_with("m68k") { // 'with_platform_dep' should only depend on 'should_not_be_included' on m68k processors // and we're not building for those, so it should be omitted let bin = &bins.get("with_platform_dep").unwrap()[0]; let dep_info = get_dependency_info(bin); eprintln!("{bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(!dep_info .packages .iter() .any(|p| p.name == "should_not_be_included")); } } #[test] fn test_build_then_runtime_dep() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/build_then_runtime_dep/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0]; let dep_info = get_dependency_info(toplevel_crate_bin); eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "build_dep" && p.kind == DependencyKind::Build)); assert!(dep_info .packages .iter() .any(|p| p.name == "runtime_dep_of_build_dep" && p.kind == DependencyKind::Build)); } #[test] fn test_runtime_then_build_dep() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/runtime_then_build_dep/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0]; let dep_info = get_dependency_info(toplevel_crate_bin); eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "runtime_dep" && p.kind == DependencyKind::Runtime)); assert!(dep_info .packages .iter() .any(|p| p.name == "build_dep_of_runtime_dep" && p.kind == DependencyKind::Build)); } #[test] fn test_custom_rustc_path() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/custom_rustc_path/Cargo.toml"); // locate rustc let rustc_path = which::which("rustc").unwrap(); // Run in workspace root with a custom path to rustc let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[("RUSTC", rustc_path.as_ref())]); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0]; let dep_info = get_dependency_info(toplevel_crate_bin); eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "runtime_dep" && p.kind == DependencyKind::Runtime)); assert!(dep_info .packages .iter() .any(|p| p.name == "build_dep_of_runtime_dep" && p.kind == DependencyKind::Build)); } #[test] fn test_workspace_member_version_info() { // Test that `/path/to/cargo-auditable rustc -vV works when compiling a workspace member // // Never happens with Cargo - it does call `rustc -vV`, // but either bypasses the wrapper or doesn't set CARGO_PRIMARY_PACKAGE=true. // However it does happen with `sccache`: // https://github.com/rust-secure-code/cargo-auditable/issues/87 let mut command = Command::new(EXE); command.env("CARGO_PRIMARY_PACKAGE", "true"); command.args(["rustc", "-vV"]); let status = command.status().unwrap(); assert!(status.success()); } #[test] fn test_wasm() { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/wasm_crate/Cargo.toml"); // Run in workspace root with default features run_cargo_auditable( workspace_cargo_toml, &["--target=wasm32-unknown-unknown"], &[], ); // check that the build types are propagated correctly let dep_info = get_dependency_info( "tests/fixtures/wasm_crate/target/wasm32-unknown-unknown/release/wasm_crate.wasm".into(), ); eprintln!("wasm_crate.wasm dependency info: {dep_info:?}"); assert_eq!(dep_info.packages.len(), 18); }