cargo-auditable-0.6.1/.cargo_vcs_info.json0000644000000001550000000000100140640ustar { "git": { "sha1": "9fa7fb3988b4d2414397b9550a3f3fd1aa1bc8f2" }, "path_in_vcs": "cargo-auditable" }cargo-auditable-0.6.1/CHANGELOG.md000064400000000000000000000036631046102023000144740ustar 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.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.1/Cargo.lock0000644000000211410000000000100120350ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", "once_cell", "version_check", ] [[package]] name = "auditable-extract" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8a62a6f4a522a2ab30b5fca049b9587228d17e4ac648106aeaf7da9b70b5e2b" dependencies = [ "binfarce", ] [[package]] name = "auditable-info" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "158d56117c834b6e90edbe4c5b5caaefff0996eebe8b952108a0a491330344ee" dependencies = [ "auditable-extract", "auditable-serde", "miniz_oxide 0.6.2", "serde_json", ] [[package]] name = "auditable-serde" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57ba7d807ad2850c436750154dbc442eb8611527deb6dfcc4e5e0bd10d5fbf28" dependencies = [ "cargo_metadata", "semver", "serde", "serde_json", "topological-sort", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "binfarce" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18464ccbb85e5dede30d70cc7676dc9950a0fb7dbf595a43d765be9123c616a2" [[package]] name = "camino" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3" dependencies = [ "serde", ] [[package]] name = "cargo-auditable" version = "0.6.1" dependencies = [ "auditable-info", "auditable-serde", "cargo_metadata", "miniz_oxide 0.5.4", "object", "pico-args", "serde", "serde_json", "which", ] [[package]] name = "cargo-platform" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08a1ec454bc3eead8719cb56e15dbbfecdbc14e4b3a3ae4936cc6e31f5fc0d07" 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.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash", ] [[package]] name = "indexmap" version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown 0.12.3", ] [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "libc" version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "miniz_oxide" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] [[package]] name = "miniz_oxide" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "object" version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "crc32fast", "hashbrown 0.13.2", "indexmap", "memchr", ] [[package]] name = "once_cell" version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[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.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "semver" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" 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.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "which" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", "once_cell", ] cargo-auditable-0.6.1/Cargo.toml0000644000000027170000000000100120700ustar # 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.1" 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.5.0" features = ["from_metadata"] [dependencies.cargo_metadata] version = "0.15" [dependencies.miniz_oxide] version = "0.5.0" [dependencies.object] version = "0.30" features = ["write"] default-features = false [dependencies.pico-args] version = "0.5" [dependencies.serde] version = "1.0.147" [dependencies.serde_json] version = "1.0.57" [dev-dependencies.auditable-info] version = "0.6.0" [dev-dependencies.cargo_metadata] version = "0.15" [dev-dependencies.which] version = "4.3.0" cargo-auditable-0.6.1/Cargo.toml.orig000064400000000000000000000017101046102023000155410ustar 00000000000000[package] name = "cargo-auditable" version = "0.6.1" 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.30", default-features = false, features = ["write"]} auditable-serde = {version = "0.5.0", path = "../auditable-serde", features = ["from_metadata"]} miniz_oxide = {version = "0.5.0"} serde_json = "1.0.57" cargo_metadata = "0.15" pico-args = "0.5" serde = "1.0.147" [dev-dependencies] cargo_metadata = "0.15" auditable-info = {version = "0.6.0", path = "../auditable-info"} which = "4.3.0" cargo-auditable-0.6.1/README.md000064400000000000000000000141211046102023000141310ustar 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. 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 ## 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. ## 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. It is also interoperable with existing tooling that consumes Cargo.lock via the [JSON-to-TOML convertor](auditable-serde/examples/json-to-toml.rs). However, we recommend supporting the format natively; the format is designed to be [very easy to parse](PARSING.md), 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](PARSING.md). See [here](PARSING.md) for documentation on parsing the data. ### 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 [will start embedding it soon.](https://github.com/rust-lang/rust/pull/97550) 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? Cargo itself is [currently in a feature freeze](https://blog.rust-lang.org/inside-rust/2022/03/31/cargo-team-changes.html). cargo-auditable-0.6.1/cargo-auditable.1000064400000000000000000000124151046102023000157630ustar 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.1/src/cargo_arguments.rs000064400000000000000000000066731046102023000172040ustar 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.1/src/cargo_auditable.rs000064400000000000000000000053151046102023000171210ustar 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.1/src/collect_audit_data.rs000064400000000000000000000101551046102023000176160ustar 00000000000000use auditable_serde::VersionInfo; use cargo_metadata::{Metadata, MetadataCommand}; use miniz_oxide::deflate::compress_to_vec_zlib; use std::{convert::TryFrom, str::from_utf8}; use crate::{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 = VersionInfo::try_from(&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.1/src/main.rs000064400000000000000000000030061046102023000147330ustar 00000000000000#![forbid(unsafe_code)] mod cargo_arguments; mod cargo_auditable; mod collect_audit_data; mod object_file; 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.1/src/object_file.rs000064400000000000000000000263461046102023000162700ustar 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::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 info["target_pointer_width"].as_str() == "32" { Architecture::Aarch64_Ilp32 } else { Architecture::Aarch64 } } "x86" => Architecture::I386, "s390x" => Architecture::S390x, "mips" => Architecture::Mips, "mips64" => Architecture::Mips64, "x86_64" => { if info["target_pointer_width"].as_str() == "32" { Architecture::X86_64_X32 } else { Architecture::X86_64 } } "powerpc" => Architecture::PowerPc, "powerpc64" => Architecture::PowerPc64, "riscv32" => Architecture::Riscv32, "riscv64" => Architecture::Riscv64, "sparc64" => Architecture::Sparc64, // Unsupported architecture. _ => return None, }; let binary_format = if target_triple.contains("-apple-") { BinaryFormat::MachO } else if target_triple.contains("-windows-") { 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); // 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 } _ => 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. fn riscv_features(target_triple: &str) -> 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"); } extensions } #[cfg(test)] mod tests { use super::*; use crate::target_info::parse_rustc_target_info; #[test] fn test_riscv_abi_detection() { // real-world target with double floats let features = riscv_features("riscv64gc-unknown-linux-gnu"); assert!(features.contains('c')); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target without floats let features = riscv_features("riscv32imac-unknown-none-elf"); assert!(features.contains('c')); assert!(!features.contains('d')); assert!(!features.contains('f')); // real-world target without floats or compression let features = riscv_features("riscv32i-unknown-none-elf"); assert!(!features.contains('c')); assert!(!features.contains('d')); assert!(!features.contains('f')); // made-up target without compression and with single floats let features = riscv_features("riscv32if-unknown-none-elf"); assert!(!features.contains('c')); 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.1/src/rustc_arguments.rs000064400000000000000000000035761046102023000172500ustar 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 pub struct RustcArgs { pub crate_name: String, pub crate_types: Vec, pub cfg: Vec, pub out_dir: PathBuf, pub target: Option, } 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 } } pub fn parse_args() -> Result { let raw_args: Vec = std::env::args_os().skip(2).collect(); let mut parser = pico_args::Arguments::from_vec(raw_args); Ok(RustcArgs { crate_name: parser.value_from_str("--crate-name")?, crate_types: parser.values_from_str("--crate-type")?, cfg: parser.values_from_str("--cfg")?, 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")?, }) } cargo-auditable-0.6.1/src/rustc_wrapper.rs000064400000000000000000000123531046102023000167140ustar 00000000000000use std::{ env, ffi::{OsStr, OsString}, process::Command, }; use crate::{collect_audit_data, object_file, rustc_arguments, 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() { // Only inject audit data into crate types 'bin' and 'cdylib' if args.crate_types.contains(&"bin".to_owned()) || args.crate_types.contains(&"cdylib".to_owned()) { // 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 = object_file::create_metadata_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 target_triple.contains("-apple-") { command.arg("-Clink-arg=-Wl,-u,_AUDITABLE_VERSION_INFO"); } else { command.arg("-Clink-arg=-Wl,--undefined=AUDITABLE_VERSION_INFO"); } } else { // create_metadata_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() .expect("Failed to invoke rustc! Make sure it's in your $PATH"); 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.1/src/target_info.rs000064400000000000000000000045131046102023000163140ustar 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.1/tests/it.rs000064400000000000000000000371771046102023000150160ustar 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"); /// 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, { let mut command = Command::new(EXE); command .arg("auditable") .arg("build") .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") { 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(package).or_insert(Vec::new()).push(binary); }); bins } 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, &["--release"], &[]); 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/runtime_then_build_dep/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()); }