├── .github ├── FUNDING.yml └── workflows │ ├── cackle.yml │ ├── ci.yml │ ├── cron.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── CONFIG.md ├── Cargo.lock ├── Cargo.toml ├── FAQ.md ├── HOW_IT_WORKS.md ├── LICENSE ├── PORTING.md ├── README.md ├── RELEASE_NOTES.md ├── SECURITY.md ├── cackle.toml ├── deny.toml ├── pre-push-tests ├── rustfmt.toml ├── scripts └── release ├── src ├── build_script_checker.rs ├── checker.rs ├── checker │ ├── api_map.rs │ └── common_prefix.rs ├── colour.rs ├── config.rs ├── config │ ├── built_in.rs │ ├── permissions.rs │ └── versions.rs ├── config_editor.rs ├── config_validation.rs ├── cowarc.rs ├── crate_index.rs ├── crate_index │ └── lib_tree.rs ├── demangle.rs ├── deps.rs ├── events.rs ├── fs.rs ├── link_info.rs ├── location.rs ├── logging.rs ├── main.rs ├── names.rs ├── outcome.rs ├── problem.rs ├── problem_store.rs ├── proxy.rs ├── proxy │ ├── cargo.rs │ ├── errors.rs │ ├── rpc.rs │ └── subprocess.rs ├── sandbox.rs ├── sandbox │ └── bubblewrap.rs ├── summary.rs ├── symbol.rs ├── symbol_graph.rs ├── symbol_graph │ ├── backtrace.rs │ ├── dwarf.rs │ └── object_file_path.rs ├── timing.rs ├── tmpdir.rs ├── ui.rs ├── ui │ ├── basic_term.rs │ ├── full_term.rs │ ├── full_term │ │ ├── problems_ui.rs │ │ └── problems_ui │ │ │ ├── diff.rs │ │ │ └── syntax_styling.rs │ └── null_ui.rs └── unsafe_checker.rs ├── test_crates ├── .cargo │ └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── a.txt ├── cackle.toml ├── crab-1 │ ├── Cargo.toml │ ├── build.rs │ ├── cackle │ │ └── export.toml │ └── src │ │ ├── impl1.rs │ │ └── lib.rs ├── crab-10 │ ├── Cargo.toml │ ├── build.rs │ ├── cpp_stuff.cc │ └── src │ │ └── lib.rs ├── crab-11 │ ├── Cargo.toml │ ├── scratch │ │ └── test-output.txt │ └── src │ │ └── lib.rs ├── crab-2 │ ├── Cargo.toml │ ├── build.rs │ ├── nothing.c │ └── src │ │ ├── bin │ │ └── c2-bin.rs │ │ └── lib.rs ├── crab-3 │ ├── Cargo.toml │ ├── build │ │ └── main.rs │ ├── settings.json │ └── src │ │ └── lib.rs ├── crab-3v2 │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── crab-4 │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── impl1.rs │ │ └── lib.rs ├── crab-5 │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── crab-6 │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── crab-7 │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── crab-8 │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs ├── crab-9 │ ├── Cargo.toml │ ├── scratch │ │ └── writable.txt │ ├── src │ │ ├── bin │ │ │ └── crab9-bin.rs │ │ └── lib.rs │ └── tests │ │ └── integration_test.rs ├── crab-bin │ ├── Cargo.toml │ ├── build.rs │ ├── scratch │ │ └── written-by-build-script.txt │ └── src │ │ ├── main.rs │ │ └── not-utf8.data ├── data │ └── random.data ├── pmacro-1 │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── res-1 │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── shared-1 │ ├── Cargo.toml │ └── src │ └── lib.rs └── tests └── integration_test.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: davidlattimore 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/cackle.yml: -------------------------------------------------------------------------------- 1 | name: Cackle 2 | 3 | # We only run Cackle when our Cargo.lock or cackle.toml are changed. A change to any Rust code could 4 | # cause some new code in one of our dependencies to become reachable where it previously wasn't, 5 | # however this is sufficiently rare that it's worthwhile not worrying about. We'd pick this up later 6 | # when Cackle gets run from our cron workflow anyway. 7 | on: 8 | push: 9 | branches: [ '**' ] 10 | paths: 11 | - Cargo.lock 12 | - cackle.toml 13 | pull_request: 14 | branches: [ '**' ] 15 | paths: 16 | - Cargo.lock 17 | - cackle.toml 18 | workflow_dispatch: 19 | 20 | env: 21 | CARGO_TERM_COLOR: always 22 | 23 | jobs: 24 | cackle: 25 | name: Cackle check and test 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dtolnay/rust-toolchain@stable 30 | id: rust-toolchain 31 | - uses: cackle-rs/cackle-action@latest 32 | # Note, we don't cache the target dir, since it currently wouldn't help. We also don't include 33 | # the toolchain or the OS in the cache key. 34 | - uses: actions/cache@v3 35 | with: 36 | path: | 37 | ~/.cargo/bin/ 38 | ~/.cargo/registry/index/ 39 | ~/.cargo/registry/cache/ 40 | ~/.cargo/git/db/ 41 | key: cackle-${{ hashFiles('**/Cargo.lock') }} 42 | - run: cargo acl -n 43 | - run: cargo acl -n test 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ '**' ] 6 | pull_request: 7 | branches: [ '**' ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | RUSTFLAGS: '-D warnings' 13 | 14 | jobs: 15 | test-stable: 16 | name: Test x86_64-linux stable 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@stable 21 | id: rust-toolchain 22 | - uses: actions/cache@v3 23 | with: 24 | path: | 25 | ~/.cargo/bin/ 26 | ~/.cargo/registry/index/ 27 | ~/.cargo/registry/cache/ 28 | ~/.cargo/git/db/ 29 | target/ 30 | key: ${{ runner.os }}-cargo-${{ steps.rust-toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }} 31 | - run: sudo apt install bubblewrap 32 | - run: cargo build --profile ci --no-default-features 33 | - run: cargo test --profile ci 34 | 35 | clippy: 36 | name: Clippy 37 | runs-on: ubuntu-22.04 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: dtolnay/rust-toolchain@stable 41 | id: rust-toolchain 42 | with: 43 | components: clippy 44 | - uses: actions/cache@v3 45 | with: 46 | path: | 47 | ~/.cargo/bin/ 48 | ~/.cargo/registry/index/ 49 | ~/.cargo/registry/cache/ 50 | ~/.cargo/git/db/ 51 | target/ 52 | key: ${{ runner.os }}-clippy-${{ steps.rust-toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }} 53 | - run: cargo clippy --target x86_64-unknown-linux-gnu 54 | 55 | rustfmt: 56 | name: Check formatting 57 | runs-on: ubuntu-22.04 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: dtolnay/rust-toolchain@stable 61 | with: 62 | components: rustfmt 63 | - run: cargo fmt --all -- --check 64 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: cackle-cron 2 | on: 3 | schedule: 4 | - cron: '36 19 * * *' 5 | workflow_dispatch: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: '-D warnings' 10 | 11 | jobs: 12 | # Run cackle after updating to the latest semver-compatible versions of packages. This aims to 13 | # detect any new permission usage introduced by our dependencies. 14 | cackle: 15 | if: github.repository == 'cackle-rs/cackle' 16 | name: cackle with latest 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@stable 21 | - uses: cackle-rs/cackle-action@latest 22 | - run: cargo update 23 | - run: cargo acl -n 24 | - run: cargo acl -n test 25 | 26 | test-nightly: 27 | if: github.repository == 'cackle-rs/cackle' 28 | name: Test Nightly 29 | runs-on: ubuntu-22.04 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dtolnay/rust-toolchain@nightly 33 | id: rust-toolchain 34 | - run: sudo apt update && sudo apt install git bubblewrap 35 | - run: cargo test --profile ci 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Trigger a release when a tag is pushed that starts with v then some number. 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.*' 6 | 7 | name: Release 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ${{ matrix.os }} 13 | permissions: 14 | contents: write # Needed for creating releases 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | - build: linux 20 | os: ubuntu-22.04 21 | target: x86_64-unknown-linux-musl 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | target: ${{ matrix.target }} 32 | 33 | - name: Extract release notes 34 | shell: bash 35 | run: | 36 | awk "/# Version ${GITHUB_REF_NAME#v}/{flag=1; next} /^$/{flag=0} flag" RELEASE_NOTES.md >REL.md 37 | 38 | - name: Build release binary 39 | uses: actions-rs/cargo@v1 40 | with: 41 | command: build 42 | args: --release --target ${{ matrix.target }} 43 | 44 | - name: Create tarballs (Unix) 45 | if: matrix.build == 'linux' 46 | run: | 47 | n="cackle-${{ github.ref_name }}-${{ matrix.target }}" 48 | mkdir "$n" 49 | cp "target/${{ matrix.target }}/release/cargo-acl" "$n" 50 | tar zcf $n.tar.gz $n 51 | 52 | - name: Release 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | body_path: REL.md 56 | files: | 57 | cackle-v*.* 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | test_crates/*/Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cackle-rs/cackle/87d0fa148466e41af58b3303c3d3df8bbcaed650/.gitmodules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "Cargo.toml", 4 | "test_crates/Cargo.toml" 5 | ], 6 | "rust-analyzer.check.command": "clippy", 7 | "rust-analyzer.imports.granularity.group": "item", 8 | "rust-analyzer.imports.granularity.enforce": true 9 | } 10 | -------------------------------------------------------------------------------- /CONFIG.md: -------------------------------------------------------------------------------- 1 | # Configuration format 2 | 3 | Cackle is configured via a `cackle.toml`, which by default is located in the package or workspace 4 | root. 5 | 6 | ## API definitions 7 | 8 | Example: 9 | 10 | ```toml 11 | [api.process] 12 | include = [ 13 | "std::process", 14 | ] 15 | exclude = [ 16 | "std::process::abort", 17 | "std::process::exit", 18 | ] 19 | ``` 20 | 21 | Here we define an API called "process". Any package that references symbols in `std::process` is 22 | considered to use this API except if the symbol referenced is `std::process::abort` or 23 | `std::process::exit`, which are excluded from the `process` API. 24 | 25 | We can define as many APIs as we like. If an API is declared, then packages need permission in order 26 | to use those APIs. 27 | 28 | ## Importing standard library API definitions 29 | 30 | Cackle has some built-in API definitions for the Rust standard library that can optionally be used. 31 | 32 | ```toml 33 | import_std = [ 34 | "fs", 35 | "net", 36 | "process", 37 | "env", 38 | "terminate", 39 | ] 40 | ``` 41 | 42 | ## Package permissions 43 | 44 | We can grant permissions to a package to use APIs or use unsafe. e.g.: 45 | 46 | ```toml 47 | [pkg.crab1] 48 | allow_unsafe = true 49 | allow_apis = [ 50 | "fs", 51 | "process", 52 | ] 53 | ``` 54 | 55 | Here we declare a package called `crab1` and say that it is allowed to use the `fs` and `process` 56 | APIs. We also say that it's allowed to use unsafe code. 57 | 58 | We can also conditionally grant permissions to use APIs only from particular kinds of binaries. For 59 | example, if we wanted to allow `crab1` to use the `fs` API, but only in code that is only reachable 60 | from test code, we can do that as follows: 61 | 62 | ```toml 63 | [pkg.crab1] 64 | from.test.allow_apis = [ 65 | "fs", 66 | ] 67 | ``` 68 | 69 | Similarly, we can allow the API to be used, but only in code that is reachable from build scripts: 70 | 71 | ```toml 72 | [pkg.crab1] 73 | from.build.allow_apis = [ 74 | "fs", 75 | ] 76 | ``` 77 | 78 | If we want to allow an API to be used specifically by `crab1`'s build script, we can do that as follows: 79 | 80 | ```toml 81 | [pkg.crab1] 82 | build.allow_apis = [ 83 | "fs", 84 | ] 85 | ``` 86 | 87 | Allowed APIs inherit as follows: 88 | 89 | * pkg.N 90 | * pkg.N.from.build (any build script) 91 | * pkg.N.build (N's build script) 92 | * pkg.N.from.test (any test) 93 | * pkg.N.test (N's tests) 94 | 95 | So granting an API usage to `pkg.N` means it can be used in any kind of binary. 96 | 97 | ## Sandbox 98 | 99 | ```toml 100 | [sandbox] 101 | kind = "Bubblewrap" 102 | ``` 103 | 104 | Here we declare that we'd like to use `Bubblewrap` (installed as `bwrap`) as our sandbox. Bubblewrap 105 | is currently the only supported kind of sandbox. The sandbox will be used for running build scripts 106 | (build.rs), running tests (with `cargo acl test`) and optionally for sandboxing rustc. 107 | 108 | If for some reason you don't want to sandbox a particular build script, you can disable the sandbox 109 | just for that build script. 110 | 111 | ```toml 112 | [pkg.foo] 113 | build.sandbox.kind = "Disabled" 114 | ``` 115 | 116 | If a build script needs network access, you can relax the sandbox to allow it as follows: 117 | 118 | ```toml 119 | [pkg.foo] 120 | build.sandbox.allow_network = true 121 | ``` 122 | 123 | Tests can also be run in a sandbox using the `test` subcommand, for example: 124 | 125 | ```sh 126 | cargo acl test 127 | ``` 128 | 129 | This builds the tests, checking the built test binaries against the permissions granted in 130 | cackle.toml, then runs the tests, with a sandbox if one is configured. 131 | 132 | The sandbox used for tests is configured under `pkg.{pkg-name}.test`. e.g.: 133 | 134 | ```toml 135 | [pkg.foo] 136 | test.sandbox.kind = "Disabled" 137 | ``` 138 | 139 | Tests and build scripts already have write access to a temporary directory, however, if for some 140 | reason they need to write to some directory in your source folder, this can be permitted as follows: 141 | 142 | ```toml 143 | [pkg.foo] 144 | test.sandbox.bind_writable = [ 145 | "test_outputs", 146 | ] 147 | ``` 148 | 149 | This will allow tests to write to the "test_outputs" subdirectory within the directory containing 150 | your `Cargo.toml`. All directories listed in `bind_writable` must exist. 151 | 152 | If you'd like to automatically create a writable directory if it doesn't already exist, then 153 | `make_writable` behaves the same, but will create the directory before starting the sandbox. 154 | 155 | ```toml 156 | [pkg.foo] 157 | test.sandbox.make_writable = [ 158 | "test_outputs", 159 | ] 160 | ``` 161 | 162 | If you need to pass particular environment variables into a sandboxed process, you can list them as 163 | follows: 164 | 165 | ```toml 166 | [pkg.foo.test.sandbox] 167 | pass_env = [ 168 | "VAR1", 169 | "VAR2", 170 | ] 171 | ``` 172 | 173 | This will cause the variables "VAR1" and "VAR2", if set, to be passed to the sandboxed process - in 174 | this case the tests for the package `foo`. 175 | 176 | ### Sandboxing rustc 177 | 178 | If you have a sandbox configuration, then from config version 2 onwards, rustc will be run in a 179 | sandbox. This means that all proc macros get sandboxed. Controlling the sandbox on a per-proc-macro 180 | basis unfortunately isn't supported yet, but hopefully will in future. This means that if you have 181 | for example one proc macro that needs network access, you'd need to enable network access for the 182 | whole rustc sandbox, which means that all proc macros would have network access. 183 | 184 | If you need to enable networking from the rustc sandbox, you can do so as follows: 185 | 186 | ```toml 187 | [rustc.sandbox] 188 | allow_network = true 189 | ``` 190 | 191 | Or if you need to completely disable the rustc sandbox: 192 | 193 | ```toml 194 | [rustc.sandbox] 195 | kind = "Disable" 196 | ``` 197 | 198 | ## Importing API definitions from an external crate 199 | 200 | If you depend on a crate that publishes `cackle/export.toml`, you can import API definitions from 201 | this as follows: 202 | 203 | ```toml 204 | [pkg.some-dependency] 205 | import = [ 206 | "fs", 207 | ] 208 | ``` 209 | 210 | API definitions imported like this will be namespaced by prefixing them with the crate that exported 211 | them. For example: 212 | 213 | ```toml 214 | [pkg.my-bin] 215 | allow_apis = [ 216 | "some-dependency::fs", 217 | ] 218 | ``` 219 | 220 | If you're the owner of a crate that provides APIs that you'd like classified, you can create 221 | `cackle/export.toml` in your crate. 222 | 223 | ## Build options 224 | 225 | ### Specifying features 226 | 227 | Features to be be passed to `cargo build` can be specified in `cackle.toml` as follows: 228 | 229 | ```toml 230 | features = ["feature1", "feature2"] 231 | ``` 232 | 233 | ### Selecting build targets 234 | 235 | Arbitrary build flags can be passed to `cargo build` using the `build_flags` option. The default is 236 | to pass `--all-targets`. 237 | 238 | ```toml 239 | [common] 240 | build_flags = ["--all-targets"] 241 | ``` 242 | 243 | If you'd like to not analyse tests, examples etc, you might override this to just the empty array 244 | `[]`. Or if you want to analyse tests, but not examples you might set it to `["--tests"]`. For 245 | available options run `cargo build --help`. 246 | 247 | ### Custom build profile 248 | 249 | By default, Cackle builds with a custom profile named "cackle" which inherits from the "dev" 250 | profile. If you'd like to use a different profile, you can override the profile in the configuration 251 | file. e.g. 252 | 253 | ```toml 254 | [common] 255 | profile = "cackle-release" 256 | ``` 257 | 258 | You can also override with the `--profile` flag, which takes precedence over the config file. 259 | 260 | Cackle supports analysing references even when inlining occurs, so it can work to some extent even 261 | with optimisations enabled, however it's more likely that you'll run into false attribution bugs, 262 | where an API usage is attributed to the wrong package. So unless you really need optimisation for 263 | some reason, it's recommended to set `opt-level = 0`. 264 | 265 | Split debug info is not yet supported, so you should turn it off. 266 | 267 | Here's an example of what you might put in your `Cargo.toml`: 268 | 269 | ```toml 270 | [profile.cackle-release] 271 | inherits = "release" 272 | opt-level = 0 273 | split-debuginfo = "off" 274 | strip = false 275 | debug = 2 276 | lto = "off" 277 | ``` 278 | 279 | ## Version number 280 | 281 | The field `common.version` is the only required field in the config file. 282 | 283 | ```toml 284 | [common] 285 | version = 2 286 | ``` 287 | 288 | If we decide to change the default values for any fields in future, we'll add a new supported version number. 289 | In this regard, `common.version` is a bit like `package.edition` in `Cargo.toml`. It's 290 | intended as a way to preserve old behaviour while making breaking changes, in particular breaking 291 | changes that might otherwise go unnoticed. 292 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-acl" 3 | version = "0.8.0" 4 | edition = "2021" 5 | rust-version = "1.74" 6 | license = "MIT OR Apache-2.0" 7 | description = "A Rust code ACL checker" 8 | readme = "README.md" 9 | repository = "https://github.com/cackle-rs/cackle" 10 | keywords = ["cargo", "plugin", "security", "supply-chain"] 11 | 12 | [dependencies] 13 | anyhow = "1.0.33" 14 | clap = { version = "4.2.1", features = [ "derive" ] } 15 | serde = { version = "1.0.136", features = [ "derive", "rc" ] } 16 | toml = "0.8.0" 17 | serde_json = "1.0.95" 18 | cargo_metadata = "0.18.0" 19 | object = "0.32.0" 20 | ar = "0.9.0" 21 | gimli = { version = "0.28.0", default-features = false, features = ["read"] } 22 | rustc-demangle = "0.1.22" 23 | once_cell = "1.17.1" 24 | is-terminal = "0.4.8" 25 | colored = "2.0.0" 26 | rustc-ap-rustc_lexer = "727.0.0" 27 | indoc = "2.0.1" 28 | log = { version = "0.4.19", features = [ "std" ] } 29 | addr2line = { version = "0.21.0", default-features = false, features = [ "std" ] } 30 | tempfile = "3.6.0" 31 | fxhash = "0.2.1" 32 | tui-input = "0.8.0" 33 | toml_edit = { version = "0.20.0" } 34 | 35 | ratatui = { version = "0.24.0", optional = true } 36 | diff = { version = "0.1.13", optional = true } 37 | crossterm = { version = "0.27.0", optional = true } 38 | 39 | [features] 40 | default = ["ui"] 41 | 42 | # Enable the "ui" subcommand. 43 | ui = ["ratatui", "diff", "crossterm"] 44 | 45 | # Build even on an operating system that isn't yet supported. Enable this feature if you're working 46 | # on porting. 47 | unsupported-os = [] 48 | 49 | # Profile used for CI. We turn off incremental compilation and debug info, since both are 50 | # unnecessary in CI and slow things down. 51 | [profile.ci] 52 | inherits = "dev" 53 | incremental = false 54 | debug = 0 55 | 56 | [profile.release] 57 | #strip = true 58 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | 3 | If you've got a question, file an issue and we'll attempt to answer it and possibly add it to this 4 | FAQ. 5 | 6 | ## Cackle reports that a crate uses unsafe, but it doesn't 7 | 8 | Try compiling the crate with `cargo rustc -- -F unsafe-code` and see what errors it reports. Use of 9 | `#[no_mangle]` or `#[link_section =...]` for example count as use of unsafe. 10 | 11 | Additionally, having the `unsafe` keyword anywhere in a crates sources will cause it to be marked as 12 | using unsafe, even if that keyword gets discarded by a macro. 13 | 14 | ## Why do some problems only show in the UI once I've fixed others 15 | 16 | Cackle analyses your dependencies as the cargo build progresses. When disallowed APIs or disallowed 17 | unsafe code is encountered, that part of the build is paused until the problems encountered are 18 | fixed or until the build is aborted. Once you fix the problems by selecting fixes through the UI, 19 | the build continues and finds more problems with later crates. 20 | 21 | ## It's very slow having to manually review each permission 22 | 23 | It's probably a good idea to spend some time thinking about whether each crate that uses an API has 24 | a legitimate use for it. That said, if you'd like to just "accept all" so to speak, you can press 25 | "a" to accept all permissions that have only a single edit. You'll still be prompted for problems 26 | that have more than one edit. When you're done, you could then look over the generated cackle.toml 27 | to see if anything jumps out at you as using an API that it shouldn't. One advantage of not doing 28 | accept-all is that the user interface gives you information about each usage, which can help in 29 | understanding why a package is using a particular API or unsafe. 30 | 31 | ## Do build scripts get run before you grant them needed permissions 32 | 33 | They don't. Compilation of a build script will pause until you grant it permission to use whatever 34 | APIs it need. If you quit out of the Cackle UI without granting required permissions, then 35 | compilation of the build script will abort. 36 | 37 | ## Why do you analyse object files rather than looking at the source AST 38 | 39 | Cackle did originally use rust-analyzer. When I switched to binary analysis, my main motivation was 40 | wanting to get accurate span information for code that originated from macros. I wouldn't rule out 41 | switching back to source analysis at some point. It's possible that we could even end up with both 42 | binary and source-based analysis. Ideally what I'd like would be if we could get rustc emit HIR in 43 | some stable format. e.g. a JSON dump of the AST with all paths resolved and with span information. 44 | 45 | ## Whats an ACL 46 | 47 | An ACL is an access-control list. It's where you have a list of permissions that you grant to some 48 | entity. In the case of Cackle, those entities are Rust packages. 49 | 50 | ## How can I get Cackle to ignore my examples, benches etc 51 | 52 | By default, Cackle runs `cargo build` with `--all-targets`. You can override this in your 53 | `cackle.toml` as follows: 54 | 55 | ```toml 56 | [common] 57 | build_flags = [] 58 | ``` 59 | -------------------------------------------------------------------------------- /HOW_IT_WORKS.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | Cackle performs a `cargo build` of your crate and wraps rustc, the linker and any build scripts. By 4 | wrapping these binaries, cackle gets an opportunity to perform analysis during the build process. 5 | 6 | ## Wrapping rustc 7 | 8 | The code for this is `proxy_rustc` in src/proxy/subprocess.rs. 9 | 10 | The first binary that cackle wraps is `rustc`. It wraps it by setting the environment variable 11 | `RUSTC_WRAPPER`, which causes cargo to invoke `cackle` instead of the real `rustc`. 12 | 13 | We adjust the command line and then invoke the real `rustc`. The most important adjustments we make 14 | to the `rustc` command line are: 15 | 16 | * We add `-Funsafe-code` unless `cackle.toml` says that the crate is allowed to use unsafe. 17 | * We override the linker used by rustc so that we can wrap that as well. 18 | * We force emitting of debug info, which is needed for later analysis. 19 | 20 | Once `rustc` completes, we parse the emitted `deps` file to get a list of the source files that were 21 | used. We then notify the parent process that `rustc` completed, telling it what source files were 22 | used. 23 | 24 | In addition to telling the parent process what source files were used, we also parse the source 25 | files, looking for the `unsafe` token. This is an additional layer of unsafe detection besides 26 | adding `-Funsafe-code` since `-Funsafe-code` is insufficient to prevent some uses of unsafe. 27 | 28 | ## Wrapping the linker 29 | 30 | The code for this is `proxy_linker` in src/proxy/subprocess.rs. 31 | 32 | When cackle is invoked as the linker, first invoke the actual linker. We then look through all the 33 | arguments passed to the linker to determine: 34 | 35 | * What object files and rlibs are being linked 36 | * What binary output (executable or shared object) is being produced 37 | 38 | We pass this information to the main cackle process. The main process stores this information for 39 | later analysis when the current `rustc` invocation finishes. The reason it doesn't analyse the 40 | linker invocation is because it needs the list of source files for the current crate, which we get 41 | from the deps file, which is written by rustc. 42 | 43 | When `rustc` does finish, the parent process the analyses the `LinkInfo` to determine what APIs were 44 | used and by which crates. For more details on this analysis, see [API analysis](#api-analysis). 45 | 46 | If the output of the linker is a build script or a test, then we rename the output and put a shell 47 | script in its place. This lets us wrap build scripts and tests. 48 | 49 | ## Wrapping build scripts 50 | 51 | The code for this is `proxy_build_script` in src/proxy/subprocess.rs. 52 | 53 | When cargo invokes a build script or test, it's actually running a shell script that we put in its 54 | place. This shell script invokes cackle, telling it what kind of binary is being invoked and where 55 | the actual binary is located. Cackle then checks to see if the binary being invoked needs to be run 56 | in a sandbox. 57 | 58 | ## API analysis 59 | 60 | The code for this is in `src/symbol_graph.rs`. 61 | 62 | When rust invokes our proxy linker, it notifies the main cackle process to tell it which binary file 63 | was linked and which object files were used as inputs. 64 | 65 | Cackle reads relocations from the object files. Relocation are generally a reference from one symbol 66 | to another, although both the source and target of the relocation can also be a linker section, with 67 | no symbol involved, which adds a little complexity. 68 | 69 | In order to check if a reference is permitted, we need to know: 70 | 71 | * What crate the reference came from 72 | * What API was referenced 73 | 74 | We determine the crate that the reference came from as follows: 75 | 76 | * The reference is always attached to a section of an object file. That section may have a symbol 77 | definition in it. If it does, we look for that symbol in the output binary. 78 | * If the output binary doesn't have that symbol, then we fall back to using debug information for 79 | the symbol. 80 | * If we have neither a symbol definition nor debug information for the symbol, then we ignore the 81 | reference, since it's from dead code and we don't care about APIs used by dead code. 82 | * If the output binary does have that symbol, then we use the offset of relocation relative to the 83 | symbol to determine the relocation address within the output binary. 84 | * Assuming we have a source location for where the relocation was applied, we use the deps files 85 | written by the rust compiler when it compiles each crate to determine which crate (or in rare 86 | circumstances crates) the source file belongs to. 87 | 88 | We determine what API was referenced as follows: 89 | 90 | * Look at the target of the relocation. If it's a section that doesn't define a symbol, then collect 91 | all symbols referenced by that section recursively until we have just a list of referenced symbols. 92 | * For each symbol, use both the demangled name of the symbol and the name provided by the debug 93 | information for that symbol. For most symbols, the symbol name is redundant as the debug name 94 | generally provides more information. There are however a few cases where the symbol contains 95 | information that the debug name doesn't, so we still need to process both. 96 | * We then split the debug name and symbol into names and look for any defined APIs in `cackle.toml` 97 | that are the prefix of these names. 98 | * Where a function uses an API and also has a name that matches that same API, we ignore the usage 99 | by that function. The usage will be attributed to whatever uses that function. The idea here is 100 | that if a crate defines a generic function, we don't want API usage to be attributed to that crate 101 | just because some other crate instantiated the generic function with some type that matched an 102 | API. e.g. if the either crate defines `Either` and some other crate uses `Either`, 103 | we want to attribute the filesystem API only to the latter crate, not to the `either` crate. 104 | -------------------------------------------------------------------------------- /PORTING.md: -------------------------------------------------------------------------------- 1 | ## Porting to other operating systems 2 | 3 | Currently only Linux is supported. The following sections describe known issues that would need to 4 | be resolved in order to get it working on other operating systems. 5 | 6 | If you'd like to work on porting, you can build on other operating systems with: 7 | 8 | ```bash 9 | cargo build --features unsupported-os 10 | ``` 11 | 12 | ### Mac 13 | 14 | Fortunately Mac, like Linux, uses DWARF for debug info. However it looks like Mac possibly doesn't 15 | put each symbol into a separate section of the object file like happens on Linux. This may cause 16 | problems for Cackle that would need to be resolved. Someone with a Mac would need to try running it 17 | and investigate what goes wrong. Please reach out if you have a Mac and would like to help with 18 | this. 19 | 20 | ### Windows 21 | 22 | Windows uses both a different object file format and a different format for debug info. The library 23 | that we use for reading object files (`object`) apparently supports the format used on Windows, 24 | although it's likely that some of our code would still need some adjusting. 25 | 26 | The larger bit of work is handling the debug info format used on Windows. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cackle / cargo acl 2 | 3 | A code ACL checker for Rust. 4 | 5 | Cackle is a tool to analyse the transitive dependencies of your crate to see what kinds of APIs each 6 | crate uses. 7 | 8 | The idea is look for crates that are using APIs that you don't think they should be using. For 9 | example a crate that from its description should just be doing some data processing, but is actually 10 | using network APIs. 11 | 12 | ## Installation 13 | 14 | Currently Cackle only works on Linux. See [PORTING.md](PORTING.md) for more details. 15 | 16 | ```sh 17 | cargo install --locked cargo-acl 18 | ``` 19 | 20 | Or if you'd like to install from git: 21 | 22 | ```sh 23 | cargo install --locked --git https://github.com/cackle-rs/cackle.git cargo-acl 24 | ``` 25 | 26 | Installing `bubblewrap` is recommended as it allows build scripts (build.rs), tests and rustc to be 27 | run inside a sandbox. 28 | 29 | On systems with `apt`, this can be done by running: 30 | 31 | ```sh 32 | sudo apt install bubblewrap 33 | ``` 34 | 35 | ## Usage 36 | 37 | From the root of your project (the directory containing `Cargo.toml`), run: 38 | 39 | ```sh 40 | cargo acl 41 | ``` 42 | 43 | This will interactively guide you through creating an initial `cackle.toml`. Some manual editing of 44 | your `cackle.toml` is recommended. In particular, you should look through your dependency tree and 45 | think about which crates export APIs that you'd like to restrict. e.g. if you're using a crate that 46 | provides network APIs, you should declare this in your config. See [CONFIG.md](CONFIG.md) for more 47 | details. 48 | 49 | ## Running from CI 50 | 51 | Cackle can be run from GitHub actions. See the instructions in the 52 | [cackle-action](https://github.com/cackle-rs/cackle-action) repository. 53 | 54 | ## Features 55 | 56 | * Checks what APIs are used by each crate in your dependency tree. 57 | * Ignores dead code, so if a crate uses an API, but in code that isn't called in your binary, then 58 | it doesn't count. 59 | * Restrict which crates are allowed to use unsafe. 60 | * A terminal UI that shows problems as they're found. 61 | * Preview the source where the API usage or unsafe was detected. 62 | * For API usages, show a backtrace of how that code is reachable. 63 | * Select from several edits that can be applied to your config file to allow the usage. 64 | * Can run build scripts, tests in a sandbox to restrict network and filesystem access. 65 | * The sandbox for each build script is configured separately, so if one build script needs extra 66 | access you can grant it to just that build script. 67 | * Can run rustc in a sandbox, thus sandboxing all proc macros. This however is currently not 68 | granular, so if one proc macro needs more access it needs to be granted to all. Fortunately proc 69 | macros that need network access are relatively rare. 70 | 71 | ## Limitations and precautions 72 | 73 | * A proc macro might detect that it's being run under Cackle and emit different code. 74 | * Even without proc macros, a crate may only use problematic APIs only in certain configurations 75 | that don't match the configuration used when you run Cackle. 76 | * This tool is intended to supplement and aid manual review of 3rd party code, not replace it. 77 | * Your configuration might miss defining an API provided by a crate as falling into a certain 78 | category that you care about. 79 | * There are undoubtedly countless ways that a determined person could circumvent detection that 80 | they're using some APIs. With time we may try to prevent such circumventions, but for now, you 81 | should definitely assume that circumvention is possible. 82 | 83 | With all these limitations, what's the point? The goal really is to just raise the bar for what's 84 | required to sneak problematic code unnoticed into some package. Use of Cackle should not replace any 85 | manual code reviews of your dependencies that you would otherwise have done. 86 | 87 | ## How it works 88 | 89 | See [HOW_IT_WORKS.md](HOW_IT_WORKS.md). 90 | 91 | ## FAQ 92 | 93 | [FAQ](FAQ.md) 94 | 95 | ## Contributing 96 | 97 | Contributions are very welcome. If you'd like to get involved, please reach out either by filing an 98 | issue or emailing David Lattimore (email address is in the commit log). 99 | 100 | ## License 101 | 102 | This software is distributed under the terms of both the MIT license and the Apache License (Version 103 | 2.0). 104 | 105 | See LICENSE for details. 106 | 107 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in 108 | this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without 109 | any additional terms or conditions. 110 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Version 0.8.0 2 | * Ignores cargo:rustc-check-cfg= build script directives. 3 | 4 | # Version 0.7.0 5 | * Fixes for recent changes in rustc nightly 6 | * `std::env` no longer included in `fs` API. It shouldn't be needed and was causing some false 7 | positives. 8 | * Added `--output-format` flag to `summary` subcommand. Thanks teromene@ 9 | 10 | # Version 0.6.0 11 | * Fixed a bug where API usages by dependencies of proc-macros were not properly checked. 12 | * Pass environment variables set by build scripts to sandboxed rustc. 13 | * Allow passing of arbitrary environment variables to sandboxed processes. 14 | * Fixed repeated running of tests when a test failed. 15 | * Fixed `cargo acl run` (`--all-targets` was being passed causing it to error). 16 | * Cargo features can now be specified via `--features` flag. 17 | 18 | # Version 0.5.0 19 | * Bypass rustup when running rustc - fixes problem where rustup fails due to running in a sandbox. 20 | 21 | # Version 0.4.0 22 | * Now sandboxes rustc (and thus proc macros) 23 | * Config version bumped to 2 (enables sandboxing of rustc) 24 | * Fixed passing arguments via `cargo acl run` 25 | * Minimum supported Rust version bumped to 1.70 26 | 27 | # Version 0.3.0 28 | * Renamed to cargo-acl 29 | * UI now only activates when necessary. 30 | * `check` and `ui` subcommands removed - now just run with no subcommand and turn the UI off with 31 | `--no-ui` or `-n`. 32 | * `cargo` subcommand removed. Instead of `cackle cargo test`, now you run `cargo acl test`. 33 | * Now available as a github action. 34 | * Allow APIs based on what kind of binary is being built (test, build script or other) 35 | * `sandbox.make_writable` can be used to create directories that need to be writable 36 | * Automatic edits now use dotted notation within `pkg.x` rather than defining a separate 37 | `pkg.x.build` etc. 38 | * Backtraces can now display sources from the rust standard library. 39 | * Various other bug fixes 40 | 41 | # Version 0.2.0 42 | * Fixed a few false-attribution problems. 43 | * Syntax highlight code snippets. 44 | * Optimised Cackle's analysis speed ~4x faster. 45 | * Added `cargo` subcommand. e.g. `cackle cargo test`. 46 | * Supports running tests in sandbox. 47 | * Sandbox config now supports making select directories writable. 48 | * Support showing a backtrace of how an API usage location is reachable. 49 | * Output from `cargo build` is now shown when running `cackle check`. 50 | * Added automated config edit to exclude a path from an API. 51 | * Binary releases now available on github. 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | 3 | It's unlikely that Cackle will ever be completely impossible to circumvent. That doesn't mean that 4 | it isn't useful though. Think of it like an antivirus that only knows about 90% of viruses. 5 | 6 | If you've found a neat way to circumvent Cackle to sneak in some API usages that it shouldn't allow, 7 | great, especially if there's a way to plug the hole. If there isn't a practical way to plug the 8 | hole, then my thoughts are that we probably shouldn't provide detailed instructions for people who 9 | want to perform supply-chain attacks. The goal is to make things as hard for them as possible. 10 | 11 | So I'd say, if the problem is fixable, feel free to just file a bug or send a PR. If it's not 12 | fixable, or you're not sure, feel free to just email me. You can find my email address by looking 13 | through the commit logs for David Lattimore. 14 | -------------------------------------------------------------------------------- /cackle.toml: -------------------------------------------------------------------------------- 1 | [common] 2 | version = 2 3 | import_std = [ 4 | "fs", 5 | "net", 6 | "process", 7 | ] 8 | 9 | [sandbox] 10 | kind = "Bubblewrap" 11 | 12 | [api.net] 13 | include = [ 14 | "mio::net", 15 | "rustix::net", 16 | ] 17 | exclude = [ 18 | "mio::net::uds", 19 | ] 20 | 21 | [api.fs] 22 | include = [ 23 | "rustix::fd", 24 | "rustix::fs", 25 | "rustix::mm", 26 | "rustix::path", 27 | "serde::de::impls::PathBufVisitor", 28 | ] 29 | no_auto_detect = [ 30 | "cargo-acl", 31 | ] 32 | 33 | [api.process] 34 | include = [ 35 | "rustix::process", 36 | ] 37 | 38 | [api.termios] 39 | include = [ 40 | "rustix::termios", 41 | ] 42 | 43 | [api.rustix-other] 44 | include = [ 45 | "rustix" 46 | ] 47 | exclude = [ 48 | "rustix::process", 49 | "rustix::fs", 50 | "rustix::fd", 51 | "rustix::mm", 52 | "rustix::path", 53 | "rustix::net", 54 | "rustix::termios", 55 | ] 56 | 57 | [pkg.serde_derive] 58 | allow_proc_macro = true 59 | 60 | [pkg.clap_derive] 61 | allow_proc_macro = true 62 | 63 | [pkg.indoc] 64 | allow_proc_macro = true 65 | 66 | [pkg.thiserror-impl] 67 | allow_proc_macro = true 68 | 69 | [pkg.unicode-ident] 70 | allow_unsafe = true 71 | 72 | [pkg.serde] 73 | allow_unsafe = true 74 | allow_apis = [ 75 | "fs", 76 | ] 77 | build.allow_apis = [ 78 | "process", 79 | ] 80 | 81 | [pkg.libc] 82 | allow_unsafe = true 83 | build.allow_apis = [ 84 | "process", 85 | ] 86 | 87 | [pkg.proc-macro2] 88 | allow_unsafe = true 89 | build.allow_apis = [ 90 | "fs", 91 | "process", 92 | ] 93 | 94 | [pkg.rustix] 95 | allow_unsafe = true 96 | build.allow_apis = [ 97 | "fs", 98 | "process", 99 | ] 100 | 101 | [pkg.autocfg] 102 | allow_apis = [ 103 | "fs", 104 | "process", 105 | ] 106 | 107 | [pkg.bitflags] 108 | allow_unsafe = true 109 | 110 | [pkg.linux-raw-sys] 111 | allow_unsafe = true 112 | 113 | [pkg.hashbrown] 114 | allow_unsafe = true 115 | 116 | [pkg.thiserror] 117 | build.allow_apis = [ 118 | "fs", 119 | "process", 120 | ] 121 | 122 | [pkg.scopeguard] 123 | allow_unsafe = true 124 | 125 | [pkg.log] 126 | allow_unsafe = true 127 | 128 | [pkg.crc32fast] 129 | allow_unsafe = true 130 | 131 | [pkg.indexmap] 132 | allow_unsafe = true 133 | 134 | [pkg.signal-hook-registry] 135 | allow_unsafe = true 136 | 137 | [pkg.syn] 138 | allow_unsafe = true 139 | 140 | [pkg.utf8parse] 141 | allow_unsafe = true 142 | 143 | [pkg.smallvec] 144 | allow_unsafe = true 145 | 146 | [pkg.mio] 147 | allow_unsafe = true 148 | 149 | [pkg.lock_api] 150 | allow_unsafe = true 151 | 152 | [pkg.is-terminal] 153 | allow_unsafe = true 154 | 155 | [pkg.camino] 156 | allow_unsafe = true 157 | allow_apis = [ 158 | "fs", 159 | ] 160 | build.allow_apis = [ 161 | "process", 162 | ] 163 | 164 | [pkg.signal-hook] 165 | allow_unsafe = true 166 | 167 | [pkg.anstyle-parse] 168 | allow_unsafe = true 169 | 170 | [pkg.parking_lot_core] 171 | allow_unsafe = true 172 | 173 | [pkg.anstyle] 174 | allow_unsafe = true 175 | 176 | [pkg.semver] 177 | allow_unsafe = true 178 | build.allow_apis = [ 179 | "process", 180 | ] 181 | 182 | [pkg.serde_json] 183 | allow_unsafe = true 184 | 185 | [pkg.static_assertions] 186 | allow_unsafe = true 187 | 188 | [pkg.parking_lot] 189 | allow_unsafe = true 190 | 191 | [pkg.clap_lex] 192 | allow_unsafe = true 193 | 194 | [pkg.ryu] 195 | allow_unsafe = true 196 | 197 | [pkg.itoa] 198 | allow_unsafe = true 199 | 200 | [pkg.anstream] 201 | allow_unsafe = true 202 | 203 | [pkg.anyhow] 204 | allow_unsafe = true 205 | build.allow_apis = [ 206 | "fs", 207 | "process", 208 | ] 209 | 210 | [pkg.twox-hash] 211 | allow_unsafe = true 212 | 213 | [pkg.stable_deref_trait] 214 | allow_unsafe = true 215 | 216 | [pkg.byteorder] 217 | allow_unsafe = true 218 | 219 | [pkg.winnow] 220 | allow_unsafe = true 221 | 222 | [pkg.crossterm] 223 | allow_unsafe = true 224 | allow_apis = [ 225 | "fs", 226 | "process", 227 | ] 228 | 229 | [pkg.flate2] 230 | allow_unsafe = true 231 | 232 | [pkg.gimli] 233 | allow_unsafe = true 234 | 235 | [pkg.ruzstd] 236 | allow_unsafe = true 237 | 238 | [pkg.toml_edit] 239 | allow_unsafe = true 240 | 241 | [pkg.memchr] 242 | allow_unsafe = true 243 | 244 | [pkg.once_cell] 245 | allow_unsafe = true 246 | 247 | [pkg.lazy_static] 248 | allow_unsafe = true 249 | 250 | [pkg.addr2line] 251 | allow_unsafe = true 252 | 253 | [pkg.object] 254 | allow_unsafe = true 255 | 256 | [pkg.colored] 257 | allow_unsafe = true 258 | 259 | [pkg.cargo-acl] 260 | allow_apis = [ 261 | "fs", 262 | "process", 263 | ] 264 | test.sandbox.bind_writable = [ 265 | "test_crates/crab-bin/scratch", 266 | "test_crates/crab-9/scratch", 267 | "test_crates/crab-11/scratch", 268 | ] 269 | test.sandbox.make_writable = [ 270 | "test_crates/custom_target_dir", 271 | ] 272 | test.sandbox.allow_network = true 273 | 274 | [pkg.clap_builder] 275 | allow_apis = [ 276 | "fs", 277 | ] 278 | 279 | [pkg.cargo_metadata] 280 | allow_apis = [ 281 | "fs", 282 | "process", 283 | ] 284 | 285 | [pkg.anstyle-query] 286 | allow_unsafe = true 287 | 288 | [pkg.paste] 289 | allow_proc_macro = true 290 | build.allow_apis = [ 291 | "process", 292 | ] 293 | 294 | [pkg.tempfile] 295 | allow_apis = [ 296 | "fs", 297 | ] 298 | 299 | [pkg.strum_macros] 300 | allow_proc_macro = true 301 | 302 | [pkg.rustversion] 303 | allow_proc_macro = true 304 | build.allow_apis = [ 305 | "fs", 306 | "process", 307 | ] 308 | 309 | [pkg.either] 310 | allow_unsafe = true 311 | 312 | [pkg.itertools] 313 | allow_unsafe = true 314 | 315 | [pkg.tui-input] 316 | allow_unsafe = true 317 | 318 | [pkg.allocator-api2] 319 | allow_unsafe = true 320 | 321 | [pkg.lru] 322 | allow_unsafe = true 323 | 324 | [pkg.derive_more] 325 | allow_proc_macro = true 326 | 327 | [pkg.foldhash] 328 | allow_unsafe = true 329 | 330 | [pkg.getrandom] 331 | allow_unsafe = true 332 | build.allow_apis = [ 333 | "process", 334 | ] 335 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | targets = [ 2 | ] 3 | feature-depth = 1 4 | 5 | [advisories] 6 | db-path = "~/.cargo/advisory-db" 7 | db-urls = ["https://github.com/rustsec/advisory-db"] 8 | vulnerability = "deny" 9 | unmaintained = "warn" 10 | yanked = "deny" 11 | notice = "warn" 12 | ignore = [ 13 | ] 14 | 15 | [licenses] 16 | unlicensed = "deny" 17 | allow = [ 18 | "MIT", 19 | "Apache-2.0", 20 | "Apache-2.0 WITH LLVM-exception", 21 | "Unicode-DFS-2016", 22 | "MPL-2.0", 23 | ] 24 | copyleft = "deny" 25 | allow-osi-fsf-free = "neither" 26 | default = "deny" 27 | confidence-threshold = 0.8 28 | 29 | [licenses.private] 30 | ignore = false 31 | registries = [ 32 | ] 33 | 34 | [bans] 35 | multiple-versions = "deny" 36 | wildcards = "deny" 37 | highlight = "all" 38 | workspace-default-features = "allow" 39 | external-default-features = "allow" 40 | skip = [ 41 | { name = "bitflags" } 42 | ] 43 | 44 | [sources] 45 | unknown-registry = "deny" 46 | unknown-git = "deny" 47 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 48 | allow-git = [] 49 | -------------------------------------------------------------------------------- /pre-push-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cargo fmt --check 4 | cargo test 5 | cargo clippy -- -D warnings 6 | 7 | if [ ! -d "target/cackle/saved-cackle-rpcs" ]; then 8 | cargo run --release -- acl --save-requests 9 | fi 10 | cargo run --release -- acl --replay-requests 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | use_field_init_shorthand = true 3 | # Uncomment once stable 4 | #imports_granularity = "Item" 5 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ $(git diff HEAD --name-only | wc -l) -ne 0 ]; then 5 | echo "Please commit all changes first" >&2 6 | exit 1 7 | fi 8 | 9 | VERSION=$(grep ^version Cargo.toml | cut -d'"' -f2) 10 | if ! head -1 RELEASE_NOTES.md | grep "# Version ${VERSION}$" >/dev/null; then 11 | echo "RELEASE_NOTES.md doesn't have Version ${VERSION} at start" >&2 12 | exit 1 13 | fi 14 | 15 | if [ $(git tag -l v${VERSION}) ]; then 16 | echo "A tag already exists for version ${VERSION}" >&2 17 | exit 1 18 | fi 19 | 20 | MIN_RUST_VER=$(grep ^rust-version Cargo.toml | cut -d'"' -f2) 21 | if [ -z "$MIN_RUST_VER" ]; then 22 | echo "Failed to determine minimum rust version" >&2 23 | exit 1 24 | fi 25 | 26 | echo "Releasing version ${VERSION} with minimum rust version ${MIN_RUST_VER}" 27 | 28 | SB=$HOME/bin/sb 29 | 30 | if [ ! -e $SB ]; then 31 | SB="" 32 | fi 33 | 34 | $SB cargo clippy -- -D warnings 35 | $SB cargo clippy --no-default-features -- -D warnings 36 | $SB cargo test 37 | $SB cargo run --release -- acl --no-ui --ignore-newer-config-versions --save-requests --fail-on-warnings 38 | $SB cargo run --release -- acl --no-ui --ignore-newer-config-versions --save-requests --fail-on-warnings test 39 | $SB cargo +${MIN_RUST_VER}-x86_64-unknown-linux-gnu test --all 40 | $SB cargo package 41 | 42 | git tag v${VERSION} 43 | git push origin 44 | git push origin refs/tags/v${VERSION} 45 | 46 | cargo publish 47 | 48 | sleep 120 49 | echo "Waiting for release build to complete..." 50 | for i in {1..1000}; do 51 | code=$(curl -o /dev/null --silent -Iw '%{http_code}' https://github.com/cackle-rs/cackle/releases/download/v${VERSION}/cackle-v${VERSION}-x86_64-unknown-linux-musl.tar.gz) 52 | if [ $code != 404 ]; then 53 | break 54 | fi 55 | if [ $i = 10 ]; then 56 | echo "Giving up waiting for release. Last code was $code" 57 | fi 58 | sleep 60 59 | done 60 | 61 | echo "Release build is available" 62 | 63 | ( 64 | cd ../cackle-action 65 | git checkout latest 66 | perl -pi -e 's/default: "0.*"$/default: "'$VERSION'"/' action.yml 67 | git add action.yml 68 | git commit -m "Release $VERSION" 69 | git push origin latest 70 | git checkout -b $VERSION 71 | git push origin $VERSION 72 | git checkout latest 73 | ) 74 | -------------------------------------------------------------------------------- /src/build_script_checker.rs: -------------------------------------------------------------------------------- 1 | use crate::config::permissions::PermSel; 2 | use crate::config::Config; 3 | use crate::crate_index::PackageId; 4 | use crate::problem::DisallowedBuildInstruction; 5 | use crate::problem::Problem; 6 | use crate::problem::ProblemList; 7 | use crate::proxy::rpc::BinExecutionOutput; 8 | use anyhow::Result; 9 | 10 | #[derive(Default)] 11 | pub(crate) struct BuildScriptReport { 12 | pub(crate) problems: ProblemList, 13 | pub(crate) env_vars: Vec, 14 | } 15 | 16 | impl BuildScriptReport { 17 | pub(crate) fn build( 18 | outputs: &BinExecutionOutput, 19 | config: &Config, 20 | ) -> Result { 21 | let mut report = BuildScriptReport::default(); 22 | let crate_sel = &outputs.crate_sel; 23 | let perm_sel = PermSel::for_build_script(crate_sel.pkg_name()); 24 | let allow_build_instructions = config 25 | .permissions 26 | .get(&perm_sel) 27 | .map(|cfg| cfg.allow_build_instructions.as_slice()) 28 | .unwrap_or(&[]); 29 | let Ok(stdout) = std::str::from_utf8(&outputs.stdout) else { 30 | report.problems.push(Problem::new(format!( 31 | "The build script `{}` emitted invalid UTF-8", 32 | crate_sel.pkg_id 33 | ))); 34 | return Ok(report); 35 | }; 36 | for line in stdout.lines() { 37 | if line.starts_with("cargo:") { 38 | report.problems.merge(check_directive( 39 | line, 40 | &crate_sel.pkg_id, 41 | allow_build_instructions, 42 | )); 43 | } 44 | if let Some(rest) = line.strip_prefix("cargo:rustc-env=") { 45 | if let Some((var_name, _value)) = rest.split_once('=') { 46 | report.env_vars.push(var_name.to_owned()); 47 | } 48 | } 49 | } 50 | Ok(report) 51 | } 52 | } 53 | 54 | /// Cargo instructions that should be harmless, so would just add noise if we were required to 55 | /// explicitly allow them. 56 | const ALWAYS_PERMITTED: &[&str] = &[ 57 | "cargo:rerun-if-", 58 | "cargo:warning", 59 | "cargo:rustc-cfg=", 60 | "cargo:rustc-check-cfg=", 61 | ]; 62 | 63 | fn check_directive( 64 | instruction: &str, 65 | pkg_id: &PackageId, 66 | allow_build_instructions: &[String], 67 | ) -> ProblemList { 68 | if ALWAYS_PERMITTED 69 | .iter() 70 | .any(|prefix| instruction.starts_with(prefix)) 71 | { 72 | return ProblemList::default(); 73 | } 74 | if allow_build_instructions 75 | .iter() 76 | .any(|i| matches(instruction, i)) 77 | { 78 | return ProblemList::default(); 79 | } 80 | Problem::DisallowedBuildInstruction(DisallowedBuildInstruction { 81 | pkg_id: pkg_id.clone(), 82 | instruction: instruction.to_owned(), 83 | }) 84 | .into() 85 | } 86 | 87 | fn matches(instruction: &str, rule: &str) -> bool { 88 | if let Some(prefix) = rule.strip_suffix('*') { 89 | instruction.starts_with(prefix) 90 | } else { 91 | instruction == rule 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use crate::config; 98 | use crate::config::SandboxConfig; 99 | use crate::crate_index::testing::pkg_id; 100 | use crate::crate_index::CrateSel; 101 | use crate::problem::DisallowedBuildInstruction; 102 | use crate::problem::Problem; 103 | use crate::problem::ProblemList; 104 | use crate::proxy::rpc::BinExecutionOutput; 105 | use std::path::PathBuf; 106 | 107 | #[track_caller] 108 | fn check(stdout: &str, config_str: &str) -> ProblemList { 109 | let config = config::testing::parse(config_str).unwrap(); 110 | let outputs = BinExecutionOutput { 111 | exit_code: 0, 112 | stdout: stdout.as_bytes().to_owned(), 113 | stderr: vec![], 114 | crate_sel: CrateSel::build_script(pkg_id("my_pkg")), 115 | sandbox_config: SandboxConfig::default(), 116 | binary_path: PathBuf::new(), 117 | sandbox_config_display: None, 118 | }; 119 | super::BuildScriptReport::build(&outputs, &config) 120 | .unwrap() 121 | .problems 122 | } 123 | 124 | #[test] 125 | fn test_empty() { 126 | assert_eq!(check("", ""), ProblemList::default()); 127 | } 128 | 129 | #[test] 130 | fn test_rerun_if_changed() { 131 | assert_eq!( 132 | check("cargo:rerun-if-changed=a.txt", ""), 133 | ProblemList::default() 134 | ); 135 | } 136 | 137 | #[test] 138 | fn test_link_directive() { 139 | assert_eq!( 140 | check("cargo:rustc-link-search=some_directory", ""), 141 | Problem::DisallowedBuildInstruction(DisallowedBuildInstruction { 142 | pkg_id: pkg_id("my_pkg"), 143 | instruction: "cargo:rustc-link-search=some_directory".to_owned(), 144 | }) 145 | .into() 146 | ); 147 | assert_eq!( 148 | check( 149 | "cargo:rustc-link-search=some_directory", 150 | r#" 151 | [pkg.my_pkg.build] 152 | allow_build_instructions = [ "cargo:rustc-link-search=some_directory" ] 153 | "# 154 | ), 155 | ProblemList::default() 156 | ); 157 | assert_eq!( 158 | check( 159 | "cargo:rustc-link-search=some_directory", 160 | r#" 161 | [pkg.my_pkg.build] 162 | allow_build_instructions = [ "cargo:rustc-link-*" ] 163 | "# 164 | ), 165 | ProblemList::default() 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/checker/api_map.rs: -------------------------------------------------------------------------------- 1 | use crate::config::ApiName; 2 | use fxhash::FxHashMap; 3 | use fxhash::FxHashSet; 4 | 5 | /// A map from a path prefix to a set of APIs. Stored as a tree where each level of the tree does 6 | /// lookup for the next part of the name. e.g. `std::path::PathBuf` would be stored as a tree with 4 7 | /// levels. The root is the empty path and should have an empty API set, then a tree node for each 8 | /// of `std`, `path` and `PathBuf`. 9 | /// 10 | /// This structure is kind of a trie. Each level however dispatches a whole word rather than a 11 | /// character like you'd have with a typical trie. 12 | /// 13 | /// Lookups are done using iterators, which allows us to efficiently find the permissions for a path 14 | /// without heap allocation. 15 | #[derive(Default)] 16 | pub(super) struct ApiMap { 17 | apis: FxHashSet, 18 | map: FxHashMap>, 19 | } 20 | 21 | impl ApiMap { 22 | /// Returns the permissions for the path produced by `key_it`. The permissions are those on 23 | /// whatever node we reach when either `key_it` ends or we have no child node for the next value 24 | /// it produces. i.e. it's the deepest node that is a prefix of the name produced by `key_it`. 25 | pub(super) fn get<'a>(&self, mut key_it: impl Iterator) -> &FxHashSet { 26 | key_it 27 | .next() 28 | .and_then(|key| self.map.get(key)) 29 | .map(|sub| sub.get(key_it)) 30 | .unwrap_or(&self.apis) as _ 31 | } 32 | 33 | /// Creates nodes to represent the name produced by `key_it`. This should be called for all path 34 | /// prefixes that we care about before calling `mut_tree` on those names path prefixes. 35 | pub(super) fn create_entry<'a>(&mut self, mut key_it: impl Iterator) { 36 | if let Some(key) = key_it.next() { 37 | self.map 38 | .entry(key.to_owned()) 39 | .or_default() 40 | .create_entry(key_it) 41 | } 42 | } 43 | 44 | /// Returns the mutable tree rooted at the path indicated by `key_it`. Panics if such a tree 45 | /// doesn't exit. i.e. you must have previously called `create_entry` for `key_it`. 46 | pub(super) fn mut_tree<'a>( 47 | &mut self, 48 | mut key_it: impl Iterator, 49 | ) -> &mut ApiMap { 50 | match key_it.next() { 51 | Some(key) => self 52 | .map 53 | .get_mut(key) 54 | .expect("mut_tree called without calling create_entry") 55 | .mut_tree(key_it), 56 | _ => self, 57 | } 58 | } 59 | 60 | /// Modifies the APIs for this node in the subtree and all child nodes. 61 | pub(super) fn update_subtree(&mut self, mutator: &impl Fn(&mut FxHashSet)) { 62 | (mutator)(&mut self.apis); 63 | for subtree in self.map.values_mut() { 64 | subtree.update_subtree(mutator); 65 | } 66 | } 67 | 68 | pub(crate) fn clear(&mut self) { 69 | self.apis.clear(); 70 | self.map.clear(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/checker/common_prefix.rs: -------------------------------------------------------------------------------- 1 | use super::ApiUsages; 2 | use crate::checker::ApiUsage; 3 | use crate::demangle::DemangleToken; 4 | use crate::names::NamesIterator; 5 | use crate::names::SymbolOrDebugName; 6 | use anyhow::Result; 7 | use fxhash::FxHashSet; 8 | 9 | /// Returns a list of name prefixes that are common to all of the from-names in the supplied API 10 | /// usages. These are candidates for names that are missing from an API when an off-tree usage is 11 | /// detected. 12 | pub(crate) fn common_from_prefixes(usages: &ApiUsages) -> Result> { 13 | common_prefixes(usages, true) 14 | } 15 | 16 | /// Returns a list of name prefixes that are common to all of the to-names in the supplied API 17 | /// usages. 18 | pub(crate) fn common_to_prefixes(usages: &ApiUsages) -> Result> { 19 | common_prefixes(usages, false) 20 | } 21 | 22 | pub(crate) fn common_prefixes(usages: &ApiUsages, from: bool) -> Result> { 23 | let mut checker = CommonPrefixChecker::default(); 24 | 25 | for usage in &usages.usages { 26 | checker.check_usage(usage, from)?; 27 | } 28 | let mut prefixes: Vec = checker.common.into_iter().map(|s| s.join("::")).collect(); 29 | prefixes.sort(); 30 | Ok(prefixes) 31 | } 32 | 33 | #[derive(Default)] 34 | struct CommonPrefixChecker<'input> { 35 | num_names: u32, 36 | common: FxHashSet>, 37 | } 38 | 39 | impl<'input> CommonPrefixChecker<'input> { 40 | fn check_usage(&mut self, usage: &'input ApiUsage, from: bool) -> Result<()> { 41 | let name = if from { &usage.from } else { &usage.to }; 42 | match name { 43 | SymbolOrDebugName::Symbol(symbol) => { 44 | self.check_names(symbol.names()?)?; 45 | } 46 | SymbolOrDebugName::DebugName(debug_name) => { 47 | self.check_names(debug_name.names_iterator())?; 48 | } 49 | } 50 | Ok(()) 51 | } 52 | 53 | fn check_names>>( 54 | &mut self, 55 | mut names: NamesIterator<'input, I>, 56 | ) -> Result<()> { 57 | let mut prefixes = FxHashSet::default(); 58 | while let Some((name, _)) = names.next_name()? { 59 | let mut parts = Vec::new(); 60 | for part in name { 61 | parts.push(part); 62 | prefixes.insert(parts.clone()); 63 | } 64 | } 65 | if self.num_names == 0 { 66 | self.common = prefixes; 67 | } else { 68 | self.common = self.common.intersection(&prefixes).cloned().collect(); 69 | } 70 | self.num_names += 1; 71 | Ok(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/colour.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use is_terminal::IsTerminal; 3 | 4 | #[derive(ValueEnum, Debug, Clone, Copy, Default)] 5 | pub(crate) enum Colour { 6 | #[default] 7 | Auto, 8 | Always, 9 | Never, 10 | } 11 | 12 | impl Colour { 13 | pub(crate) fn should_use_colour(&self) -> bool { 14 | match self { 15 | Colour::Auto => panic!("Missing call to Colour::detect"), 16 | Colour::Always => true, 17 | Colour::Never => false, 18 | } 19 | } 20 | 21 | /// Resolves "auto" to either "always" or "never" depending on if the output is a tty. Also 22 | /// updates the colored crate's override if the flag was already set to "never" or "always". 23 | pub(crate) fn detect(self) -> Self { 24 | match self { 25 | Colour::Auto => { 26 | if std::io::stdout().is_terminal() { 27 | Colour::Always 28 | } else { 29 | Colour::Never 30 | } 31 | } 32 | Colour::Always => { 33 | colored::control::set_override(true); 34 | self 35 | } 36 | Colour::Never => { 37 | colored::control::set_override(false); 38 | self 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/config/built_in.rs: -------------------------------------------------------------------------------- 1 | use super::ApiConfig; 2 | use super::ApiName; 3 | use super::ApiPath; 4 | use std::collections::BTreeMap; 5 | 6 | pub(crate) fn get_built_ins() -> BTreeMap { 7 | let mut result = BTreeMap::new(); 8 | result.insert( 9 | ApiName::from("fs"), 10 | perm( 11 | &[ 12 | "std::fs", 13 | "std::os::linux::fs", 14 | "std::os::unix::fs", 15 | "std::os::unix::io", 16 | "std::os::wasi::fs", 17 | "std::os::wasi::io", 18 | "std::os::windows::fs", 19 | "std::os::windows::io", 20 | "std::path", 21 | ], 22 | &[], 23 | ), 24 | ); 25 | result.insert(ApiName::from("env"), perm(&["std::env"], &[])); 26 | result.insert( 27 | ApiName::from("net"), 28 | perm( 29 | &["std::net", "std::os::wasi::net", "std::os::windows::net"], 30 | &[], 31 | ), 32 | ); 33 | result.insert( 34 | ApiName::from("unix_sockets"), 35 | perm(&["std::os::unix::net"], &[]), 36 | ); 37 | result.insert( 38 | ApiName::from("process"), 39 | perm( 40 | &[ 41 | "std::process", 42 | "std::unix::process", 43 | "std::windows::process", 44 | ], 45 | &["std::process::abort", "std::process::exit"], 46 | ), 47 | ); 48 | result.insert( 49 | ApiName::from("terminate"), 50 | perm(&["std::process::abort", "std::process::exit"], &[]), 51 | ); 52 | result 53 | } 54 | 55 | fn perm(include: &[&str], exclude: &[&str]) -> ApiConfig { 56 | ApiConfig { 57 | include: include.iter().map(|s| ApiPath::from_str(s)).collect(), 58 | exclude: exclude.iter().map(|s| ApiPath::from_str(s)).collect(), 59 | no_auto_detect: Vec::new(), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/config/versions.rs: -------------------------------------------------------------------------------- 1 | use super::RawConfig; 2 | use super::SandboxKind; 3 | use crate::config_editor::ConfigEditor; 4 | use anyhow::Result; 5 | 6 | pub(crate) const MAX_VERSION: i64 = VERSIONS.len() as i64 - 1; 7 | 8 | #[derive(Clone)] 9 | pub(crate) struct Version { 10 | pub(crate) number: i64, 11 | 12 | /// A description of what has changed from the previous version. 13 | pub(crate) change_notes: &'static str, 14 | 15 | /// A transformation that will be applied to the user's config at runtime if they're using an 16 | /// earlier version. 17 | apply_fn: fn(&mut RawConfig), 18 | 19 | /// A transformation that can be applied to edit the user's config in order to preserve the old 20 | /// behaviour when updating to this version. This should do semantically the same thing as 21 | /// apply_fn, but editing the config TOML instead of updating the runtime representation. 22 | update_fn: fn(&mut ConfigEditor) -> Result<()>, 23 | } 24 | 25 | pub(crate) const VERSIONS: &[Version] = &[ 26 | Version { 27 | number: 0, 28 | change_notes: "", 29 | apply_fn: |_| {}, 30 | update_fn: |_| Ok(()), 31 | }, 32 | Version { 33 | number: 1, 34 | change_notes: "", 35 | apply_fn: |_| {}, 36 | update_fn: |_| Ok(()), 37 | }, 38 | Version { 39 | number: 2, 40 | change_notes: "\ 41 | rustc.sandbox.kind now inherits from sandbox.kind. So if you have a default sandbox \ 42 | configured, updating to version 2 or higher will mean that rustc will now be \ 43 | sandboxed.", 44 | apply_fn: |config| { 45 | if config.rustc.sandbox.kind.is_none() { 46 | config.rustc.sandbox.kind = Some(SandboxKind::Disabled); 47 | } 48 | }, 49 | update_fn: |editor| { 50 | let table = editor.table(["rustc", "sandbox"].into_iter())?; 51 | if !table.contains_key("kind") { 52 | table.insert("kind", toml_edit::value("Disabled")); 53 | } 54 | Ok(()) 55 | }, 56 | }, 57 | ]; 58 | 59 | impl Version { 60 | pub(crate) fn apply(&self, editor: &mut ConfigEditor) -> Result<()> { 61 | (self.update_fn)(editor)?; 62 | editor.set_version(self.number) 63 | } 64 | } 65 | 66 | /// Applies whatever changes are necessary in order to bring us up to the latest version while 67 | /// preserving the behaviour of whatever version the user specified. Note, common.version isn't 68 | /// updated, since we need to keep that as what the user has in their config file, otherwise we 69 | /// won't be able to detect that newer versions are available and offer them to the user. 70 | pub(crate) fn apply_runtime_patches(config: &mut RawConfig) { 71 | for version in &VERSIONS[(config.common.version as usize + 1).clamp(0, VERSIONS.len())..] { 72 | (version.apply_fn)(config); 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::VERSIONS; 79 | use crate::config_editor::ConfigEditor; 80 | use indoc::indoc; 81 | 82 | /// Tests that apply_fn and update_fn do semantically the same thing. 83 | #[test] 84 | fn test_edit_consistency() { 85 | let mut toml = indoc! {r#" 86 | [common] 87 | version = 1 88 | "#} 89 | .to_owned(); 90 | for version in &VERSIONS[2..] { 91 | let mut editor = ConfigEditor::from_toml_string(&toml).unwrap(); 92 | version.apply(&mut editor).unwrap(); 93 | let edited_toml = editor.to_toml(); 94 | 95 | let mut config = crate::config::parse_raw(&toml).unwrap(); 96 | (version.apply_fn)(&mut config); 97 | let edited_config = crate::config::parse_raw(&edited_toml).unwrap(); 98 | assert_eq!(config.common.version, version.number - 1); 99 | config.common.version = version.number; 100 | assert_eq!(config, edited_config); 101 | 102 | toml = edited_toml; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/config_validation.rs: -------------------------------------------------------------------------------- 1 | use crate::config::ApiName; 2 | use crate::config::Config; 3 | use crate::config::MAX_VERSION; 4 | use fxhash::FxHashSet; 5 | use std::fmt::Display; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct InvalidConfig { 11 | config_path: PathBuf, 12 | problems: Vec, 13 | } 14 | 15 | #[derive(Debug)] 16 | enum Problem { 17 | UnknownPermission(ApiName), 18 | DuplicateAllowedApi(ApiName), 19 | UnsupportedVersion(i64), 20 | InvalidPkgSelector(String), 21 | } 22 | 23 | pub(crate) fn validate(config: &Config, config_path: &Path) -> Result<(), InvalidConfig> { 24 | let mut problems = Vec::new(); 25 | if config.raw.common.version < 1 || config.raw.common.version > MAX_VERSION { 26 | problems.push(Problem::UnsupportedVersion(config.raw.common.version)); 27 | } 28 | let permission_names: FxHashSet<_> = config.raw.apis.keys().collect(); 29 | for (perm_sel, crate_config) in &config.permissions_no_inheritance.packages { 30 | let mut used = FxHashSet::default(); 31 | for permission_name in &crate_config.allow_apis { 32 | if !permission_names.contains(permission_name) { 33 | problems.push(Problem::UnknownPermission(permission_name.clone())); 34 | } 35 | if !used.insert(permission_name) { 36 | problems.push(Problem::DuplicateAllowedApi(permission_name.clone())) 37 | } 38 | } 39 | if crate_config.build.is_some() { 40 | problems.push(Problem::InvalidPkgSelector(format!("{perm_sel}.build"))); 41 | } 42 | if crate_config.test.is_some() { 43 | problems.push(Problem::InvalidPkgSelector(format!("{perm_sel}.test"))); 44 | } 45 | if crate_config.from.is_some() { 46 | problems.push(Problem::InvalidPkgSelector(format!("{perm_sel}.dep"))); 47 | } 48 | } 49 | if problems.is_empty() { 50 | Ok(()) 51 | } else { 52 | Err(InvalidConfig { 53 | config_path: config_path.to_owned(), 54 | problems, 55 | }) 56 | } 57 | } 58 | 59 | impl Display for InvalidConfig { 60 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 | writeln!(f, "Invalid config {}", self.config_path.display())?; 62 | for problem in &self.problems { 63 | match problem { 64 | Problem::UnknownPermission(x) => write!(f, " Unknown permission '{}'", x.name)?, 65 | Problem::DuplicateAllowedApi(x) => { 66 | write!(f, " API allowed more than once '{}'", x.name)? 67 | } 68 | Problem::UnsupportedVersion(version) => { 69 | write!(f, " Unsupported version '{version}'")? 70 | } 71 | Problem::InvalidPkgSelector(sel) => { 72 | write!(f, " Unsupported package selector `pkg.{sel}`")? 73 | } 74 | } 75 | } 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl std::error::Error for InvalidConfig {} 81 | -------------------------------------------------------------------------------- /src/cowarc.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | use std::ops::Deref; 3 | use std::sync::Arc; 4 | 5 | pub(crate) type Bytes<'data> = CowArc<'data, [u8]>; 6 | pub(crate) type Utf8Bytes<'data> = CowArc<'data, str>; 7 | 8 | /// Provides a way to hold some data that is either borrowed, or on the heap. In this respect, it's 9 | /// a bit like a Cow, but uses reference counting when storing on the heap, so Clone is always O(1). 10 | /// Unlike a Cow, it's never writable, so the 'w' in the name is perhaps not technically accurate. 11 | /// 12 | /// The intended use case is to create some data by borrowing, then later, when non-borrowed data is 13 | /// needed, call `to_heap`, which gives us an instance with a 'static lifetime. 14 | /// 15 | /// All comparison, hashing etc is done based on the stored data. i.e. two instances that store the 16 | /// data, with one being on the heap and the other not, should behave the same. 17 | #[derive(Debug)] 18 | pub(crate) enum CowArc<'data, T: ?Sized> { 19 | Heap(Arc), 20 | Borrowed(&'data T), 21 | } 22 | 23 | impl CowArc<'_, T> { 24 | /// Returns a reference to the data contained within. Note that the returned reference is valid 25 | /// for the lifetime of `self`, not for 'data, since if we're stored on the heap, we can't 26 | /// provide a reference that's valid for 'data, which may be longer (and likely 'static). 27 | pub(crate) fn data(&self) -> &T { 28 | match self { 29 | CowArc::Heap(data) => data, 30 | CowArc::Borrowed(data) => data, 31 | } 32 | } 33 | } 34 | 35 | impl Clone for CowArc<'_, T> { 36 | fn clone(&self) -> Self { 37 | match self { 38 | Self::Heap(arg0) => Self::Heap(Arc::clone(arg0)), 39 | Self::Borrowed(arg0) => Self::Borrowed(arg0), 40 | } 41 | } 42 | } 43 | 44 | impl CowArc<'_, [V]> { 45 | /// Create an instance that is heap-allocated and reference counted and thus can be used beyond 46 | /// the lifetime 'data. 47 | pub(crate) fn to_heap(&self) -> CowArc<'static, [V]> { 48 | CowArc::Heap(match self { 49 | CowArc::Heap(data) => Arc::clone(data), 50 | CowArc::Borrowed(data) => Arc::from(*data), 51 | }) 52 | } 53 | } 54 | 55 | impl CowArc<'_, str> { 56 | /// Create an instance that is heap-allocated and reference counted and thus can be used beyond 57 | /// the lifetime 'data. 58 | pub(crate) fn to_heap(&self) -> CowArc<'static, str> { 59 | CowArc::Heap(match self { 60 | CowArc::Heap(data) => Arc::clone(data), 61 | CowArc::Borrowed(data) => Arc::from(*data), 62 | }) 63 | } 64 | } 65 | 66 | impl Deref for CowArc<'_, T> { 67 | type Target = T; 68 | 69 | fn deref(&self) -> &Self::Target { 70 | self.data() 71 | } 72 | } 73 | 74 | impl PartialEq for CowArc<'_, T> { 75 | fn eq(&self, other: &Self) -> bool { 76 | self.data().eq(other.data()) 77 | } 78 | } 79 | 80 | impl Hash for CowArc<'_, T> { 81 | fn hash(&self, state: &mut H) { 82 | self.data().hash(state); 83 | } 84 | } 85 | 86 | impl PartialOrd for CowArc<'_, T> { 87 | fn partial_cmp(&self, other: &Self) -> Option { 88 | self.data().partial_cmp(other.data()) 89 | } 90 | } 91 | 92 | impl Eq for CowArc<'_, T> {} 93 | 94 | impl Ord for CowArc<'_, T> { 95 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 96 | self.data().cmp(other.data()) 97 | } 98 | } 99 | 100 | #[test] 101 | fn comparison() { 102 | fn hash(sym: &Bytes) -> u64 { 103 | let mut hasher = std::collections::hash_map::DefaultHasher::new(); 104 | sym.hash(&mut hasher); 105 | hasher.finish() 106 | } 107 | use std::hash::Hash; 108 | use std::hash::Hasher; 109 | 110 | let sym1 = Bytes::Borrowed(b"sym1"); 111 | let sym2 = Bytes::Borrowed(b"sym2"); 112 | assert_eq!(sym1, sym1.to_heap()); 113 | assert_eq!(sym1, sym1.clone()); 114 | assert!(sym1 < sym2); 115 | assert!(sym1.to_heap() < sym2); 116 | assert!(sym1 < sym2.to_heap()); 117 | assert_eq!(hash(&sym1), hash(&sym1.to_heap())); 118 | } 119 | -------------------------------------------------------------------------------- /src/crate_index/lib_tree.rs: -------------------------------------------------------------------------------- 1 | use super::PackageId; 2 | use anyhow::anyhow; 3 | use anyhow::bail; 4 | use anyhow::Context; 5 | use anyhow::Result; 6 | use cargo_metadata::semver::Version; 7 | use fxhash::FxHashMap; 8 | use fxhash::FxHashSet; 9 | use std::path::Path; 10 | use std::process::Command; 11 | use std::sync::Arc; 12 | 13 | /// Information about what packages depend on what other packages with the current configuration. 14 | /// i.e. it excludes dependencies that are not currently enabled. 15 | #[derive(Default, Debug)] 16 | pub(super) struct LibTree { 17 | /// Map from the lib-name to the package ID that provides it. e.g. the lib name might be 18 | /// "futures_util" and the package might be "futures-util" version 0.3.38. Note the hyphen vs 19 | /// underscore. 20 | pub(super) lib_name_to_pkg_id: FxHashMap, PackageId>, 21 | pub(super) pkg_transitive_deps: FxHashMap>>, 22 | } 23 | 24 | impl LibTree { 25 | pub(super) fn from_workspace( 26 | dir: &Path, 27 | pkg_name_to_ids: &FxHashMap, Vec>, 28 | ) -> Result { 29 | let builder = LibTreeBuilder { 30 | stack: Vec::new(), 31 | tree: LibTree::default(), 32 | pkg_name_to_ids, 33 | }; 34 | builder.build(dir) 35 | } 36 | } 37 | 38 | struct LibTreeBuilder<'a> { 39 | stack: Vec, 40 | tree: LibTree, 41 | pkg_name_to_ids: &'a FxHashMap, Vec>, 42 | } 43 | 44 | impl LibTreeBuilder<'_> { 45 | fn build(mut self, dir: &Path) -> Result { 46 | let output = Command::new("cargo") 47 | .current_dir(dir) 48 | .arg("tree") 49 | .args(["--edges", "normal,no-proc-macro"]) 50 | .args(["--prefix", "depth"]) 51 | .args(["--format", " {lib} {p}"]) 52 | .output() 53 | .context("Failed to run cargo tree")?; 54 | 55 | let stdout = std::str::from_utf8(&output.stdout) 56 | .context("Got non-utf-8 output from `cargo tree`")?; 57 | for line in stdout.lines() { 58 | self.process_line(line)?; 59 | } 60 | self.pop_to_level(0); 61 | Ok(self.tree) 62 | } 63 | 64 | fn process_line(&mut self, line: &str) -> Result<()> { 65 | let mut parts = line.split(' '); 66 | let (Some(level), Some(lib_name), Some(pkg_name), Some(version_str)) = 67 | (parts.next(), parts.next(), parts.next(), parts.next()) 68 | else { 69 | return Ok(()); 70 | }; 71 | let level = level 72 | .parse::() 73 | .context("Invalid depth in `cargo tree` output")? 74 | + 1; 75 | let Some(version_str) = version_str.strip_prefix('v') else { 76 | bail!("Version string `{version_str}` from `cargo tree` doesn't start with v"); 77 | }; 78 | let version = Version::parse(version_str) 79 | .context("Failed to parse package version from `cargo tree`")?; 80 | let packages_with_name = self.pkg_name_to_ids.get(pkg_name).with_context(|| { 81 | format!( 82 | "`cargo tree` output contained package `{}` not in `cargo metadata` output", 83 | pkg_name 84 | ) 85 | })?; 86 | let package_id = packages_with_name 87 | .iter() 88 | .find(|id| id.version == version) 89 | .ok_or_else(|| { 90 | anyhow!( 91 | "`cargo tree` listed `{pkg_name}` version `{version_str}` not in \ 92 | `cargo metadata` output" 93 | ) 94 | })?; 95 | let lib_name: Arc = if lib_name.is_empty() { 96 | // Bin packages don't have a lib name, so we just produce one ourselves from the package 97 | // name. 98 | Arc::from(pkg_name.replace('-', "_")) 99 | } else { 100 | Arc::from(lib_name) 101 | }; 102 | self.tree 103 | .lib_name_to_pkg_id 104 | .insert(lib_name.clone(), package_id.clone()); 105 | if level - 1 <= self.stack.len() { 106 | self.pop_to_level(level - 1); 107 | } 108 | // If we're already encountered this package before, then add all of its transitive 109 | // dependencies to the deps of all the packages that depend on it (up the stack). 110 | if let Some(deps) = self.tree.pkg_transitive_deps.get(package_id) { 111 | for entry in &mut self.stack { 112 | entry.deps.extend(deps.iter().cloned()); 113 | } 114 | } 115 | // Add the current package as a dependency of all the packages on the stack. 116 | for entry in &mut self.stack { 117 | entry.deps.insert(lib_name.clone()); 118 | } 119 | if self.stack.len() < level { 120 | self.stack.push(StackEntry { 121 | package_id: package_id.clone(), 122 | deps: Default::default(), 123 | }); 124 | } 125 | Ok(()) 126 | } 127 | 128 | fn pop_to_level(&mut self, level: usize) { 129 | while self.stack.len() > level { 130 | let entry = self.stack.pop().unwrap(); 131 | self.tree 132 | .pkg_transitive_deps 133 | .entry(entry.package_id) 134 | .or_insert(entry.deps); 135 | } 136 | } 137 | } 138 | 139 | #[derive(Debug)] 140 | struct StackEntry { 141 | package_id: PackageId, 142 | deps: FxHashSet>, 143 | } 144 | -------------------------------------------------------------------------------- /src/deps.rs: -------------------------------------------------------------------------------- 1 | //! Locates and parses depinfo emitted by the rust compiler. 2 | 3 | use anyhow::anyhow; 4 | use anyhow::bail; 5 | use anyhow::Context; 6 | use anyhow::Result; 7 | use std::path::Path; 8 | use std::path::PathBuf; 9 | 10 | /// Uses the supplied rustc arguments to determine where the deps file will be located, then reads 11 | /// it and extracts the paths of all the source files. 12 | pub(crate) fn source_files_from_rustc_args( 13 | args: impl Iterator, 14 | ) -> Result> { 15 | let Some(deps_path) = deps_path_from_rustc_args(args)? else { 16 | return Ok(vec![]); 17 | }; 18 | let deps = std::fs::read_to_string(&deps_path) 19 | .with_context(|| format!("Failed to read deps file `{}`", deps_path.display()))?; 20 | Ok(parse_deps(&deps)? 21 | .into_iter() 22 | .flat_map(|dep| dep.canonicalize()) 23 | .collect()) 24 | } 25 | 26 | fn parse_deps(deps_text: &str) -> Result> { 27 | let mut deps = Vec::new(); 28 | for line in deps_text.lines() { 29 | if let Some(filename) = line.strip_suffix(':') { 30 | deps.push(PathBuf::from(filename)); 31 | } 32 | } 33 | Ok(deps) 34 | } 35 | 36 | fn deps_path_from_rustc_args(mut args: impl Iterator) -> Result> { 37 | let mut crate_name = None; 38 | let mut extra = String::new(); 39 | let mut out_dir = None; 40 | let mut emit_dep_info = false; 41 | while let Some(arg) = args.next() { 42 | if arg == "-C" { 43 | let Some(arg) = args.next() else { 44 | bail!("Missing argument to -C"); 45 | }; 46 | if let Some(rest) = arg.strip_prefix("extra-filename=") { 47 | extra = rest.to_owned(); 48 | } 49 | } else if arg == "--out-dir" { 50 | let Some(arg) = args.next() else { 51 | bail!("Missing argument to --out-dir"); 52 | }; 53 | out_dir = Some(arg); 54 | } else if arg == "--crate-name" { 55 | let Some(arg) = args.next() else { 56 | bail!("Missing argument to --crate-name"); 57 | }; 58 | crate_name = Some(arg); 59 | } else if arg.starts_with("--emit=") { 60 | emit_dep_info = arg.contains("dep-info"); 61 | } 62 | } 63 | if !emit_dep_info { 64 | return Ok(None); 65 | } 66 | let crate_name = crate_name.ok_or_else(|| anyhow!("Missing --crate-name"))?; 67 | let out_dir = out_dir.ok_or_else(|| anyhow!("Missing --out-dir"))?; 68 | Ok(Some( 69 | Path::new(&out_dir).join(format!("{crate_name}{extra}.d")), 70 | )) 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::deps_path_from_rustc_args; 76 | use super::parse_deps; 77 | use anyhow::Result; 78 | use std::path::PathBuf; 79 | 80 | fn deps_path(args: &[&str]) -> Result> { 81 | deps_path_from_rustc_args(args.iter().map(|s| s.to_string())) 82 | } 83 | 84 | #[test] 85 | fn test_source_files_from_rustc_args() { 86 | let deps_path = deps_path(&[ 87 | "rustc", 88 | "--emit=dep-info,link", 89 | "--crate-name", 90 | "foo", 91 | "-C", 92 | "extra-filename=-0188200cb614ae3d", 93 | "--out-dir", 94 | "/some/directory/target/debug/deps", 95 | ]) 96 | .unwrap(); 97 | assert_eq!( 98 | deps_path, 99 | Some(PathBuf::from( 100 | "/some/directory/target/debug/deps/foo-0188200cb614ae3d.d" 101 | )) 102 | ); 103 | } 104 | 105 | #[test] 106 | fn test_source_files_from_rustc_args_missing_crate_name() { 107 | assert!(deps_path(&[ 108 | "rustc", 109 | "--emit=dep-info,link", 110 | "-C", 111 | "extra-filename=-0188200cb614ae3d", 112 | "--out-dir", 113 | "/some/directory/target/debug/deps", 114 | ]) 115 | .is_err()); 116 | } 117 | 118 | #[test] 119 | fn test_source_files_from_rustc_args_missing_out_dir() { 120 | assert!(deps_path(&[ 121 | "rustc", 122 | "--emit=dep-info,link", 123 | "--crate-name", 124 | "foo", 125 | "-C", 126 | "extra-filename=-0188200cb614ae3d", 127 | ]) 128 | .is_err()); 129 | } 130 | 131 | #[test] 132 | fn test_source_files_from_rustc_args_no_dep_info() { 133 | assert_eq!(deps_path(&[]).unwrap(), None); 134 | } 135 | 136 | fn path_strings(input: &[PathBuf]) -> Vec<&str> { 137 | input.iter().filter_map(|path| path.to_str()).collect() 138 | } 139 | 140 | #[test] 141 | fn test_parse_deps() { 142 | let deps = parse_deps(indoc::indoc! {r#" 143 | /some/path/foo-1235.rmeta: foo/src/lib.rs /some/absolute/path/extra.rs 144 | 145 | /some/path/foo-1235.rlib: foo/src/lib.rs /some/absolute/path/extra.rs 146 | 147 | foo/src/lib.rs: 148 | /some/absolute/path/extra.rs: 149 | 150 | # env-dep:OUT_DIR=/some/path/target/debug/build/foo-1235/out 151 | "#}) 152 | .unwrap(); 153 | assert_eq!( 154 | path_strings(&deps), 155 | &["foo/src/lib.rs", "/some/absolute/path/extra.rs"] 156 | ) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 2 | pub(crate) enum AppEvent { 3 | /// Shutdown in progress. The UI should close. 4 | Shutdown, 5 | /// New problems have been added to the problem store. 6 | ProblemsAdded, 7 | } 8 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use anyhow::Result; 3 | use std::path::Path; 4 | 5 | /// Writes `contents` to `path`. The write is first done to a temporary filename then renamed to 6 | /// `path`. This means that other processes will either see the old contents or the new contents, 7 | /// but should never see a half-written version of the new contents. 8 | pub(crate) fn write_atomic(path: &Path, contents: &str) -> Result<()> { 9 | let tmp_path = path.with_extension("tmp"); 10 | std::fs::write(&tmp_path, contents) 11 | .with_context(|| format!("Failed to write `{}`", tmp_path.display()))?; 12 | std::fs::rename(&tmp_path, path).with_context(|| { 13 | format!( 14 | "Failed to rename `{}` to `{}`", 15 | tmp_path.display(), 16 | path.display() 17 | ) 18 | })?; 19 | Ok(()) 20 | } 21 | 22 | pub(crate) fn read_to_string(path: &Path) -> Result { 23 | std::fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display())) 24 | } 25 | 26 | pub(crate) fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { 27 | let path = path.as_ref(); 28 | std::fs::write(path, contents).with_context(|| format!("Failed to write {}", path.display())) 29 | } 30 | -------------------------------------------------------------------------------- /src/link_info.rs: -------------------------------------------------------------------------------- 1 | use crate::crate_index::CrateSel; 2 | use anyhow::bail; 3 | use anyhow::Result; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | 10 | /// Information about a linker invocation. 11 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] 12 | pub(crate) struct LinkInfo { 13 | pub(crate) crate_sel: CrateSel, 14 | pub(crate) object_paths: Vec, 15 | pub(crate) output_file: Arc, 16 | is_shared: bool, 17 | } 18 | 19 | impl LinkInfo { 20 | pub(crate) fn from_env() -> Result { 21 | let crate_sel = CrateSel::from_env()?; 22 | let object_paths = std::env::args() 23 | .skip(1) 24 | .map(PathBuf::from) 25 | .filter(|path| has_supported_extension(path)) 26 | .collect(); 27 | Ok(LinkInfo { 28 | crate_sel, 29 | object_paths, 30 | output_file: get_output_file()?, 31 | is_shared: get_is_shared(), 32 | }) 33 | } 34 | 35 | /// Filters `object_paths` to just those under `dir`. 36 | pub(crate) fn object_paths_under(&self, dir: &Path) -> Vec { 37 | self.object_paths 38 | .iter() 39 | .filter_map(|path| path.canonicalize().ok()) 40 | .filter(|path| path.starts_with(dir)) 41 | .collect() 42 | } 43 | 44 | /// Returns whether the output of the linker is an executable (not a shared object). 45 | pub(crate) fn is_executable(&self) -> bool { 46 | !self.is_shared 47 | } 48 | } 49 | 50 | fn get_output_file() -> Result> { 51 | let mut args = std::env::args(); 52 | while let Some(arg) = args.next() { 53 | if arg == "-o" { 54 | if let Some(output) = args.next() { 55 | return Ok(Arc::from(Path::new(&output))); 56 | } 57 | } 58 | } 59 | bail!("Failed to find output file in linker command line"); 60 | } 61 | 62 | fn get_is_shared() -> bool { 63 | std::env::args().any(|arg| arg == "-shared") 64 | } 65 | 66 | fn has_supported_extension(path: &Path) -> bool { 67 | const EXTENSIONS: &[&str] = &["rlib", "o"]; 68 | path.extension() 69 | .and_then(|ext| ext.to_str()) 70 | .is_some_and(|ext| EXTENSIONS.contains(&ext)) 71 | } 72 | -------------------------------------------------------------------------------- /src/location.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use std::fmt::Display; 4 | use std::path::Path; 5 | use std::sync::Arc; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 8 | pub(crate) struct SourceLocation { 9 | filename: Arc, 10 | line: u32, 11 | column: Option, 12 | } 13 | 14 | impl SourceLocation { 15 | pub(crate) fn new>>(filename: P, line: u32, column: Option) -> Self { 16 | Self { 17 | filename: filename.into(), 18 | line, 19 | column, 20 | } 21 | } 22 | 23 | pub(crate) fn filename(&self) -> &Path { 24 | &self.filename 25 | } 26 | 27 | pub(crate) fn line(&self) -> u32 { 28 | self.line 29 | } 30 | 31 | pub(crate) fn column(&self) -> Option { 32 | self.column 33 | } 34 | 35 | pub(crate) fn with_sysroot(&self, sysroot: &Path) -> Self { 36 | if !self.filename.starts_with("/rustc/") { 37 | return self.clone(); 38 | } 39 | let mut filename = sysroot.join("lib/rustlib/src/rust"); 40 | filename.extend(self.filename.iter().skip(3)); 41 | Self { 42 | filename: Arc::from(filename.as_path()), 43 | line: self.line, 44 | column: self.column, 45 | } 46 | } 47 | } 48 | 49 | impl Display for SourceLocation { 50 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 51 | write!(f, "{} [{}", self.filename.display(), self.line)?; 52 | if let Some(column) = self.column { 53 | write!(f, ":{}", column)?; 54 | } 55 | write!(f, "]") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use anyhow::Context; 3 | use anyhow::Result; 4 | use clap::ValueEnum; 5 | use std::io::Write; 6 | use std::path::Path; 7 | use std::sync::Mutex; 8 | use std::time::Instant; 9 | 10 | /// Our own enum for log level filtering. We only provide the levels that we actually use. We also 11 | /// derive `clap::ValueEnum` and `Default`, which `log::LevelFilter` doesn't. 12 | #[derive(ValueEnum, Debug, Clone, Copy, Default)] 13 | pub(crate) enum LevelFilter { 14 | #[default] 15 | Info, 16 | Debug, 17 | Trace, 18 | } 19 | 20 | pub(crate) fn init(output_path: &Path, level: LevelFilter) -> Result<()> { 21 | let file = std::fs::File::create(output_path) 22 | .with_context(|| format!("Failed to write log file `{}`", output_path.display()))?; 23 | log::set_boxed_logger(Box::new(FileLogger { 24 | file: Mutex::new(file), 25 | start: Instant::now(), 26 | })) 27 | .map_err(|_| anyhow!("Failed to set logger"))?; 28 | log::set_max_level(level.into()); 29 | Ok(()) 30 | } 31 | 32 | struct FileLogger { 33 | file: Mutex, 34 | start: Instant, 35 | } 36 | 37 | impl log::Log for FileLogger { 38 | fn enabled(&self, metadata: &log::Metadata) -> bool { 39 | metadata.level() <= log::max_level() 40 | } 41 | 42 | fn log(&self, record: &log::Record) { 43 | if !self.enabled(record.metadata()) { 44 | return; 45 | } 46 | // If a write to our log file fails, there's not a lot we can do, so we just ignore it. 47 | let mut file = self.file.lock().unwrap(); 48 | let _ = writeln!( 49 | file, 50 | "{:0.3}: {} - {}", 51 | self.start.elapsed().as_secs_f32(), 52 | record.level(), 53 | record.args() 54 | ); 55 | } 56 | 57 | fn flush(&self) { 58 | let _ = self.file.lock().unwrap().flush(); 59 | } 60 | } 61 | 62 | impl From for log::LevelFilter { 63 | fn from(val: LevelFilter) -> Self { 64 | match val { 65 | LevelFilter::Info => log::LevelFilter::Info, 66 | LevelFilter::Debug => log::LevelFilter::Debug, 67 | LevelFilter::Trace => log::LevelFilter::Trace, 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/outcome.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use std::fmt::Display; 4 | 5 | pub(crate) const SUCCESS: ExitCode = ExitCode(0); 6 | pub(crate) const FAILURE: ExitCode = ExitCode(-1); 7 | 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 9 | pub(crate) enum Outcome { 10 | Continue, 11 | GiveUp, 12 | } 13 | 14 | impl Outcome { 15 | pub(crate) fn and(self, other: Outcome) -> Outcome { 16 | if matches!((self, other), (Outcome::Continue, Outcome::Continue)) { 17 | Outcome::Continue 18 | } else { 19 | Outcome::GiveUp 20 | } 21 | } 22 | } 23 | 24 | /// Our own representation for an ExitCode. We don't use ExitStatus from the standard library 25 | /// because sometimes we need to construct an ExitCode ourselves. 26 | #[derive(Debug, PartialEq, Eq)] 27 | pub(crate) struct ExitCode(pub(crate) i32); 28 | 29 | impl ExitCode { 30 | pub(crate) fn code(&self) -> i32 { 31 | self.0 32 | } 33 | 34 | pub(crate) fn is_ok(&self) -> bool { 35 | self.0 == 0 36 | } 37 | } 38 | 39 | impl From for ExitCode { 40 | fn from(status: std::process::ExitStatus) -> Self { 41 | ExitCode(status.code().unwrap_or(-1)) 42 | } 43 | } 44 | 45 | impl Display for ExitCode { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | write!(f, "{}", self.0) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/proxy/cargo.rs: -------------------------------------------------------------------------------- 1 | use crate::config::CommonConfig; 2 | use crate::Args; 3 | use clap::Parser; 4 | use std::path::Path; 5 | use std::process::Command; 6 | 7 | /// The name of the default cargo profile that we use. 8 | pub(crate) const DEFAULT_PROFILE_NAME: &str = "cackle"; 9 | pub(crate) const PROFILE_NAME_ENV: &str = "CACKLE_BUILD_PROFILE"; 10 | 11 | #[derive(Parser, Debug, Clone)] 12 | pub(crate) struct CargoOptions { 13 | #[clap(allow_hyphen_values = true)] 14 | remaining: Vec, 15 | } 16 | 17 | /// Returns the build profile to use. Order of priority is (1) command line (2) cackle.toml (3) 18 | /// default. 19 | pub(crate) fn profile_name<'a>(args: &'a Args, config: &'a CommonConfig) -> &'a str { 20 | args.profile 21 | .as_deref() 22 | .or(config.profile.as_deref()) 23 | .unwrap_or(DEFAULT_PROFILE_NAME) 24 | } 25 | 26 | pub(crate) fn command( 27 | base_command: &str, 28 | dir: &Path, 29 | args: &Args, 30 | config: &CommonConfig, 31 | ) -> Command { 32 | let mut command = Command::new("cargo"); 33 | command.current_dir(dir); 34 | if args.colour.should_use_colour() { 35 | command.arg("--color=always"); 36 | } 37 | let extra_args = match &args.command { 38 | Some(crate::Command::Test(cargo_options)) => { 39 | command.arg("test"); 40 | cargo_options.remaining.as_slice() 41 | } 42 | Some(crate::Command::Run(cargo_options)) => { 43 | command.arg("run"); 44 | cargo_options.remaining.as_slice() 45 | } 46 | _ => { 47 | command.arg(base_command); 48 | &[] 49 | } 50 | }; 51 | command 52 | .arg("--config") 53 | .arg(format!("profile.{DEFAULT_PROFILE_NAME}.inherits=\"dev\"")); 54 | // Optimisation would likely make it harder to figure out where code came from. 55 | command 56 | .arg("--config") 57 | .arg(format!("profile.{DEFAULT_PROFILE_NAME}.opt-level=0")); 58 | // We currently always clean before we build, so incremental compilation would just be a waste. 59 | command 60 | .arg("--config") 61 | .arg(format!("profile.{DEFAULT_PROFILE_NAME}.incremental=false")); 62 | // We don't currently support split debug info. 63 | command.arg("--config").arg("split-debuginfo=\"off\""); 64 | let profile = profile_name(args, config); 65 | command.arg("--profile").arg(profile); 66 | command.env(PROFILE_NAME_ENV, profile); 67 | command.args(extra_args); 68 | command 69 | } 70 | -------------------------------------------------------------------------------- /src/proxy/errors.rs: -------------------------------------------------------------------------------- 1 | //! Handles parsing of errors from rustc. 2 | 3 | use crate::location::SourceLocation; 4 | use anyhow::Context; 5 | use anyhow::Result; 6 | use serde::Deserialize; 7 | use std::path::Path; 8 | 9 | /// Returns source locations for all errors related to use of unsafe code in `output`, which should 10 | /// be the output from rustc with --error-format=json. 11 | pub(crate) fn get_disallowed_unsafe_locations( 12 | rustc_output: &std::process::Output, 13 | ) -> Result> { 14 | let stderr = 15 | std::str::from_utf8(&rustc_output.stderr).context("rustc emitted invalid UTF-8")?; 16 | Ok(get_disallowed_unsafe_locations_str(stderr)) 17 | } 18 | 19 | fn get_disallowed_unsafe_locations_str(output: &str) -> Vec { 20 | let mut locations = Vec::new(); 21 | //let workspace_root = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_default()); 22 | for line in output.lines() { 23 | let Ok(message) = serde_json::from_str::(line) else { 24 | continue; 25 | }; 26 | if message.level == "error" && message.code.code == "unsafe_code" { 27 | if let Some(first_span) = message.spans.first() { 28 | let filename = Path::new(&first_span.file_name); 29 | locations.push(SourceLocation::new( 30 | std::fs::canonicalize(filename).unwrap_or_else(|_| filename.to_owned()), 31 | first_span.line_start, 32 | Some(first_span.column_start), 33 | )); 34 | } 35 | } 36 | } 37 | locations 38 | } 39 | 40 | #[derive(Deserialize, PartialEq, Eq, Debug)] 41 | struct Message { 42 | code: Code, 43 | level: String, 44 | spans: Vec, 45 | } 46 | 47 | #[derive(Deserialize, PartialEq, Eq, Debug)] 48 | struct Code { 49 | code: String, 50 | } 51 | 52 | #[derive(Deserialize, PartialEq, Eq, Debug)] 53 | struct SpannedMessage { 54 | file_name: String, 55 | line_start: u32, 56 | column_start: u32, 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | fn test_empty() { 65 | assert_eq!(get_disallowed_unsafe_locations_str(""), vec![]); 66 | } 67 | 68 | #[test] 69 | fn test_unsafe_error() { 70 | let json = r#"{ 71 | "code": {"code": "unsafe_code"}, 72 | "level": "error", 73 | "spans": [ 74 | { 75 | "file_name": "src/main.rs", 76 | "line_start": 10, 77 | "column_start": 20 78 | } 79 | ], 80 | "rendered": "Stuff that we don't parse" 81 | }"# 82 | .replace('\n', ""); 83 | assert_eq!( 84 | get_disallowed_unsafe_locations_str(&json), 85 | vec![SourceLocation::new( 86 | std::fs::canonicalize(Path::new("src/main.rs")).unwrap(), 87 | 10, 88 | Some(20), 89 | )] 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/proxy/rpc.rs: -------------------------------------------------------------------------------- 1 | //! Defines the communication protocol between the proxy subprocesses and the parent process. 2 | 3 | use crate::config::SandboxConfig; 4 | use crate::crate_index::CrateSel; 5 | use crate::link_info::LinkInfo; 6 | use crate::location::SourceLocation; 7 | use crate::outcome::Outcome; 8 | use anyhow::Context; 9 | use anyhow::Result; 10 | use serde::de::DeserializeOwned; 11 | use serde::Deserialize; 12 | use serde::Serialize; 13 | use std::io::Read; 14 | use std::io::Write; 15 | use std::os::unix::net::UnixStream; 16 | use std::path::PathBuf; 17 | 18 | /// A communication channel to the main Cackle process. 19 | pub(crate) struct RpcClient { 20 | socket_path: PathBuf, 21 | } 22 | 23 | impl RpcClient { 24 | pub(crate) fn new(socket_path: PathBuf) -> Self { 25 | RpcClient { socket_path } 26 | } 27 | 28 | /// Advises the parent process that the specified crate uses unsafe. 29 | pub(crate) fn crate_uses_unsafe( 30 | &self, 31 | crate_sel: &CrateSel, 32 | locations: Vec, 33 | ) -> Result { 34 | let mut ipc = self.connect()?; 35 | let request = Request::CrateUsesUnsafe(UnsafeUsage { 36 | crate_sel: crate_sel.clone(), 37 | locations, 38 | }); 39 | write_to_stream(&request, &mut ipc)?; 40 | read_from_stream(&mut ipc) 41 | } 42 | 43 | pub(crate) fn rustc_started(&self, crate_sel: &CrateSel) -> Result { 44 | let mut ipc = self.connect()?; 45 | let request = Request::RustcStarted(crate_sel.clone()); 46 | write_to_stream(&request, &mut ipc)?; 47 | read_from_stream(&mut ipc) 48 | } 49 | 50 | pub(crate) fn linker_invoked(&self, info: LinkInfo) -> Result { 51 | let mut ipc = self.connect()?; 52 | write_to_stream(&Request::LinkerInvoked(info), &mut ipc)?; 53 | read_from_stream(&mut ipc) 54 | } 55 | 56 | pub(crate) fn bin_execution_complete(&self, info: BinExecutionOutput) -> Result { 57 | let mut ipc = self.connect()?; 58 | write_to_stream(&Request::BinExecutionComplete(info), &mut ipc)?; 59 | read_from_stream(&mut ipc) 60 | } 61 | 62 | pub(crate) fn rustc_complete(&self, info: RustcOutput) -> Result { 63 | let mut ipc = self.connect()?; 64 | write_to_stream(&Request::RustcComplete(info), &mut ipc)?; 65 | read_from_stream(&mut ipc) 66 | } 67 | 68 | /// Creates a new connection to the socket. We only send a single request/response on each 69 | /// connection because it makes things simpler. In general a single request/response is all we 70 | /// need anyway. 71 | fn connect(&self) -> Result { 72 | UnixStream::connect(&self.socket_path).with_context(|| { 73 | format!( 74 | "Failed to connect to socket `{}`", 75 | self.socket_path.display() 76 | ) 77 | }) 78 | } 79 | } 80 | 81 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 82 | pub(crate) enum Request { 83 | /// Advises that the specified crate failed to compile because it uses unsafe. 84 | CrateUsesUnsafe(UnsafeUsage), 85 | LinkerInvoked(LinkInfo), 86 | BinExecutionComplete(BinExecutionOutput), 87 | RustcStarted(CrateSel), 88 | RustcComplete(RustcOutput), 89 | } 90 | 91 | /// The output from running a binary such as a build script or a test. 92 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash)] 93 | pub(crate) struct BinExecutionOutput { 94 | pub(crate) exit_code: i32, 95 | pub(crate) stdout: Vec, 96 | pub(crate) stderr: Vec, 97 | pub(crate) crate_sel: CrateSel, 98 | pub(crate) sandbox_config: SandboxConfig, 99 | pub(crate) binary_path: PathBuf, 100 | /// A display string for how the sandbox was configured (e.g. the command line). Only present if 101 | /// the exit code is non-zero. 102 | pub(crate) sandbox_config_display: Option, 103 | } 104 | 105 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash)] 106 | pub(crate) struct RustcOutput { 107 | pub(crate) crate_sel: CrateSel, 108 | pub(crate) source_paths: Vec, 109 | } 110 | 111 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash)] 112 | pub(crate) struct UnsafeUsage { 113 | pub(crate) crate_sel: CrateSel, 114 | pub(crate) locations: Vec, 115 | } 116 | 117 | /// Writes `value` to `stream`. The format used is the length followed by `value` serialised as 118 | /// JSON. 119 | pub(crate) fn write_to_stream(value: &T, stream: &mut impl Write) -> Result<()> { 120 | let serialized = serde_json::to_string(value)?; 121 | stream.write_all(&serialized.len().to_le_bytes())?; 122 | stream.write_all(serialized.as_bytes())?; 123 | Ok(()) 124 | } 125 | 126 | /// Reads a value of type `T` from `stream`. Format is the same as for `write_to_stream`. 127 | pub(crate) fn read_from_stream(stream: &mut impl Read) -> Result { 128 | let mut len_bytes = [0u8; std::mem::size_of::()]; 129 | stream.read_exact(&mut len_bytes)?; 130 | let len = usize::from_le_bytes(len_bytes); 131 | let mut buf = vec![0u8; len]; 132 | stream.read_exact(&mut buf)?; 133 | let serialized = std::str::from_utf8(&buf)?; 134 | serde_json::from_str(serialized).with_context(|| format!("Invalid message `{serialized}`")) 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::*; 140 | use std::path::Path; 141 | 142 | #[test] 143 | fn serialize_deserialize() { 144 | let req = Request::CrateUsesUnsafe(UnsafeUsage { 145 | crate_sel: CrateSel::primary(crate::crate_index::testing::pkg_id("foo")), 146 | locations: vec![SourceLocation::new(Path::new("src/main.rs"), 42, None)], 147 | }); 148 | let mut buf = Vec::new(); 149 | write_to_stream(&req, &mut buf).unwrap(); 150 | 151 | let req2 = read_from_stream(&mut buf.as_slice()).unwrap(); 152 | 153 | assert_eq!(req, req2); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/sandbox/bubblewrap.rs: -------------------------------------------------------------------------------- 1 | use super::Sandbox; 2 | use anyhow::Context; 3 | use anyhow::Result; 4 | use std::ffi::OsStr; 5 | use std::ffi::OsString; 6 | use std::fmt::Display; 7 | use std::path::Path; 8 | use std::process::Command; 9 | 10 | #[derive(Default)] 11 | pub(super) struct Bubblewrap { 12 | args: Vec, 13 | } 14 | 15 | impl Bubblewrap { 16 | fn arg>(&mut self, arg: S) { 17 | self.args.push(arg.as_ref().to_owned()); 18 | } 19 | 20 | fn command(&self, command: &Command) -> Command { 21 | let mut bwrap_command = Command::new("bwrap"); 22 | bwrap_command 23 | .args(["--unshare-all"]) 24 | .args(["--uid", "1000"]) 25 | .args(["--gid", "1000"]) 26 | .args(["--hostname", "none"]) 27 | .args(["--new-session"]) 28 | .args(["--clearenv"]) 29 | .args(&self.args) 30 | .args(["--dev", "/dev"]) 31 | .args(["--proc", "/proc"]); 32 | for (var_name, value) in command.get_envs() { 33 | if let Some(value) = value { 34 | bwrap_command.arg("--setenv").arg(var_name).arg(value); 35 | } else { 36 | bwrap_command.arg("--unsetenv").arg(var_name); 37 | } 38 | } 39 | bwrap_command 40 | .arg("--") 41 | .arg(command.get_program()) 42 | .args(command.get_args()); 43 | bwrap_command 44 | } 45 | } 46 | 47 | impl Sandbox for Bubblewrap { 48 | fn raw_arg(&mut self, arg: &OsStr) { 49 | self.args.push(arg.to_owned()); 50 | } 51 | 52 | fn tmpfs(&mut self, dir: &Path) { 53 | self.arg("--tmpfs"); 54 | self.arg(dir); 55 | } 56 | 57 | fn ro_bind(&mut self, dir: &Path) { 58 | if !dir.exists() { 59 | return; 60 | } 61 | self.arg("--ro-bind"); 62 | self.arg(dir); 63 | self.arg(dir); 64 | } 65 | 66 | fn writable_bind(&mut self, dir: &Path) { 67 | self.arg("--bind-try"); 68 | self.arg(dir); 69 | self.arg(dir); 70 | } 71 | 72 | fn set_env(&mut self, var: &OsStr, value: &OsStr) { 73 | self.arg("--setenv"); 74 | self.arg(var); 75 | self.arg(value); 76 | } 77 | 78 | fn allow_network(&mut self) { 79 | self.arg("--share-net"); 80 | } 81 | 82 | fn run(&self, command: &Command) -> Result { 83 | let mut command = self.command(command); 84 | command.output().with_context(|| { 85 | format!( 86 | "Failed to run sandbox command: {}", 87 | Path::new(command.get_program()).display() 88 | ) 89 | }) 90 | } 91 | 92 | fn display_to_run(&self, command: &Command) -> Box { 93 | Box::new(CommandDisplay { 94 | command: self.command(command), 95 | }) 96 | } 97 | } 98 | 99 | pub(crate) fn has_bwrap() -> bool { 100 | std::process::Command::new("bwrap") 101 | .arg("--version") 102 | .output() 103 | .ok() 104 | .is_some_and(|output| output.status.success()) 105 | } 106 | 107 | struct CommandDisplay { 108 | command: Command, 109 | } 110 | 111 | impl Display for CommandDisplay { 112 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 113 | write!(f, "{}", self.command.get_program().to_string_lossy())?; 114 | for arg in self.command.get_args() { 115 | let arg = arg.to_string_lossy(); 116 | if arg.contains(' ') || arg.contains('"') || arg.is_empty() { 117 | // Use debug print, since that gives us quotes. 118 | write!(f, " {:?}", arg)?; 119 | } else { 120 | // Print without quotes, since it probably isn't necessary. 121 | write!(f, " {arg}")? 122 | } 123 | } 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/summary.rs: -------------------------------------------------------------------------------- 1 | use crate::config::permissions::PermSel; 2 | use crate::config::Config; 3 | use crate::config::PackageConfig; 4 | use crate::crate_index::CrateIndex; 5 | use clap::{Parser, ValueEnum}; 6 | use fxhash::FxHashMap; 7 | use serde_json::Value; 8 | use std::collections::BTreeMap; 9 | use std::collections::HashMap; 10 | use std::fmt::Display; 11 | 12 | /// Counts of how many packages in the dependency tree use different permissions, how many use no 13 | /// special permissions etc. 14 | #[derive(serde::Serialize)] 15 | pub(crate) struct Summary { 16 | packages: Vec, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] 20 | pub enum OutputFormat { 21 | /// Print output in a human-readable form. 22 | Human, 23 | /// Print output in a machine-readable form with minimal extra context. 24 | Json, 25 | } 26 | 27 | #[derive(Parser, Debug, Clone)] 28 | pub(crate) struct SummaryOptions { 29 | /// Print summary by package. 30 | #[clap(long)] 31 | by_package: bool, 32 | 33 | /// Print summary by permission. 34 | #[clap(long)] 35 | by_permission: bool, 36 | 37 | /// Call out proc macros with other permissions. 38 | #[clap(long)] 39 | impure_proc_macros: bool, 40 | 41 | /// Print counts. 42 | #[clap(long)] 43 | counts: bool, 44 | 45 | /// Print all summary kinds. This is the default if no options are specified. 46 | #[clap(long)] 47 | full: bool, 48 | 49 | /// Whether to print headers for each summary section. This is forced on if more than one 50 | /// summary is selected. 51 | #[clap(long)] 52 | print_headers: bool, 53 | 54 | /// The format of the output 55 | #[clap(long, value_enum, action)] 56 | #[clap(default_value_t = OutputFormat::Human)] 57 | output_format: OutputFormat, 58 | } 59 | 60 | #[derive(serde::Serialize)] 61 | struct PackageSummary { 62 | pub(crate) name: PermSel, 63 | pub(crate) permissions: Vec, 64 | } 65 | 66 | impl PackageSummary { 67 | fn is_proc_macro_with_other_permissions(&self) -> bool { 68 | self.permissions.iter().any(|p| p.starts_with("proc_macro")) 69 | && self 70 | .permissions 71 | .iter() 72 | .any(|p| !p.starts_with("proc_macro") && !p.ends_with("[build]")) 73 | } 74 | } 75 | 76 | impl Summary { 77 | pub(crate) fn new(crate_index: &CrateIndex, config: &Config) -> Self { 78 | let pkg_configs: FxHashMap<&PermSel, &PackageConfig> = 79 | config.permissions.packages.iter().collect(); 80 | let mut packages: Vec = crate_index 81 | .package_ids() 82 | .map(|pkg_id| { 83 | let mut permissions = Vec::new(); 84 | let pkg_name = PermSel::for_primary(pkg_id.name_str()); 85 | let build_script_name = PermSel::for_build_script(pkg_id.name_str()); 86 | for (crate_name, suffix) in [(&pkg_name, ""), (&build_script_name, "[build]")] { 87 | if let Some(pkg_config) = pkg_configs.get(&crate_name) { 88 | if pkg_config.allow_proc_macro { 89 | permissions.push(format!("proc_macro{suffix}")); 90 | } 91 | if pkg_config.allow_unsafe { 92 | permissions.push(format!("unsafe{suffix}")); 93 | } 94 | for api in &pkg_config.allow_apis { 95 | permissions.push(format!("{api}{suffix}")); 96 | } 97 | } 98 | } 99 | PackageSummary { 100 | name: pkg_name, 101 | permissions, 102 | } 103 | }) 104 | .collect(); 105 | packages.sort_by(|a, b| a.name.cmp(&b.name)); 106 | 107 | Self { packages } 108 | } 109 | 110 | pub(crate) fn print(&self, options: &SummaryOptions) { 111 | let options = options.with_defaults(); 112 | let mut json_map = HashMap::new(); 113 | 114 | if options.by_package { 115 | if options.output_format == OutputFormat::Human { 116 | if options.print_headers { 117 | println!("=== Permissions by package ==="); 118 | } 119 | self.print_by_crate(); 120 | } else { 121 | self.json_print_by_crate(&mut json_map); 122 | } 123 | } 124 | if options.by_permission { 125 | if options.output_format == OutputFormat::Human { 126 | if options.print_headers { 127 | println!("=== Packages by permission ==="); 128 | } 129 | self.print_by_permission(); 130 | } else { 131 | self.json_print_by_permission(&mut json_map); 132 | } 133 | } 134 | if options.impure_proc_macros { 135 | if options.output_format == OutputFormat::Human { 136 | if options.print_headers { 137 | println!("=== Proc macros with other permissions ==="); 138 | } 139 | self.print_impure_proc_macros(); 140 | } else { 141 | self.json_print_impure_proc_macros(&mut json_map); 142 | } 143 | } 144 | if options.counts { 145 | if options.output_format == OutputFormat::Human { 146 | if options.print_headers { 147 | println!("=== Permission counts ==="); 148 | } 149 | println!("{self}"); 150 | } else { 151 | self.json_print_count(&mut json_map); 152 | } 153 | } 154 | 155 | if !json_map.is_empty() { 156 | println!("{}", serde_json::to_string_pretty(&json_map).unwrap()); 157 | } 158 | } 159 | 160 | fn print_by_crate(&self) { 161 | for pkg in &self.packages { 162 | println!("{}: {}", pkg.name, pkg.permissions.join(", ")); 163 | } 164 | } 165 | 166 | fn json_print_by_crate(&self, json_map: &mut HashMap<&str, Value>) { 167 | let mut map = HashMap::new(); 168 | for pkg in &self.packages { 169 | map.insert(&pkg.name.package_name, &pkg.permissions); 170 | } 171 | json_map.insert( 172 | "permissions_by_package", 173 | serde_json::to_value(&map).unwrap(), 174 | ); 175 | } 176 | 177 | fn print_impure_proc_macros(&self) { 178 | for pkg in &self.packages { 179 | if pkg.is_proc_macro_with_other_permissions() { 180 | println!("{}: {}", pkg.name, pkg.permissions.join(", ")); 181 | } 182 | } 183 | } 184 | 185 | fn json_print_impure_proc_macros(&self, json_map: &mut HashMap<&str, Value>) { 186 | let mut map = HashMap::new(); 187 | for pkg in &self.packages { 188 | if pkg.is_proc_macro_with_other_permissions() { 189 | map.insert(&pkg.name.package_name, &pkg.permissions); 190 | } 191 | } 192 | json_map.insert("impure_proc_macros", serde_json::to_value(&map).unwrap()); 193 | } 194 | 195 | fn print_by_permission(&self) { 196 | let mut by_permission: BTreeMap<&str, Vec> = BTreeMap::new(); 197 | for pkg in &self.packages { 198 | for perm in &pkg.permissions { 199 | by_permission 200 | .entry(perm) 201 | .or_default() 202 | .push(pkg.name.to_string()); 203 | } 204 | } 205 | for (perm, packages) in by_permission { 206 | println!("{perm}: {}", packages.join(", ")); 207 | } 208 | } 209 | 210 | fn json_print_by_permission(&self, json_map: &mut HashMap<&str, Value>) { 211 | let mut by_permission: BTreeMap<&str, Vec> = BTreeMap::new(); 212 | for pkg in &self.packages { 213 | for perm in &pkg.permissions { 214 | by_permission 215 | .entry(perm) 216 | .or_default() 217 | .push(pkg.name.to_string()); 218 | } 219 | } 220 | json_map.insert( 221 | "impure_proc_macros", 222 | serde_json::to_value(&by_permission).unwrap(), 223 | ); 224 | } 225 | 226 | fn json_print_count(&self, json_map: &mut HashMap<&str, Value>) { 227 | let mut map = HashMap::new(); 228 | for pkg in &self.packages { 229 | map.insert(&pkg.name.package_name, &pkg.permissions); 230 | } 231 | json_map.insert("permission_count", serde_json::to_value(&map).unwrap()); 232 | } 233 | } 234 | 235 | impl SummaryOptions { 236 | fn with_defaults(&self) -> SummaryOptions { 237 | let mut updated = self.clone(); 238 | match self.num_selected() { 239 | 0 => { 240 | updated.full = true; 241 | updated.print_headers = true; 242 | } 243 | 1 => {} 244 | _ => updated.print_headers = true, 245 | } 246 | if updated.full { 247 | updated.by_package = true; 248 | updated.by_permission = true; 249 | updated.impure_proc_macros = true; 250 | updated.counts = true; 251 | } 252 | updated 253 | } 254 | 255 | /// Returns the number of output options that are enabled. 256 | fn num_selected(&self) -> u32 { 257 | let mut count = 0; 258 | if self.full { 259 | return 2; 260 | } 261 | if self.by_package { 262 | count += 1; 263 | } 264 | if self.counts { 265 | count += 1; 266 | } 267 | if self.by_permission { 268 | count += 1; 269 | } 270 | if self.impure_proc_macros { 271 | count += 1; 272 | } 273 | count 274 | } 275 | } 276 | 277 | impl Display for Summary { 278 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 279 | writeln!(f, "num_packages: {}", self.packages.len())?; 280 | writeln!( 281 | f, 282 | "no_special_permissions: {}", 283 | self.packages 284 | .iter() 285 | .filter(|pkg| pkg.permissions.is_empty()) 286 | .count() 287 | )?; 288 | writeln!( 289 | f, 290 | "proc_macros_with_other_permissions: {}", 291 | self.packages 292 | .iter() 293 | .filter(|p| p.is_proc_macro_with_other_permissions()) 294 | .count() 295 | )?; 296 | Ok(()) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/symbol.rs: -------------------------------------------------------------------------------- 1 | use crate::cowarc::Bytes; 2 | use crate::demangle::DemangleIterator; 3 | use crate::demangle::DemangleToken; 4 | use crate::names::NamesIterator; 5 | use anyhow::Result; 6 | use rustc_demangle::demangle; 7 | use std::fmt::Debug; 8 | use std::fmt::Display; 9 | use std::str::Utf8Error; 10 | 11 | /// A symbol from an object file. The symbol might be valid UTF-8 or not. It also may or may not be 12 | /// mangled. Storage may be borrowed or on the heap. 13 | #[derive(Eq, Clone, Ord, PartialEq, PartialOrd, Hash)] 14 | pub(crate) struct Symbol<'data> { 15 | bytes: Bytes<'data>, 16 | } 17 | 18 | impl Symbol<'_> { 19 | pub(crate) fn borrowed(data: &[u8]) -> Symbol { 20 | Symbol { 21 | bytes: Bytes::Borrowed(data), 22 | } 23 | } 24 | 25 | /// Create an instance that is heap-allocated and reference counted and thus can be used beyond 26 | /// the lifetime 'data. 27 | pub(crate) fn to_heap(&self) -> Symbol<'static> { 28 | Symbol { 29 | bytes: self.bytes.to_heap(), 30 | } 31 | } 32 | 33 | /// Returns the data that we store. 34 | fn data(&self) -> &[u8] { 35 | &self.bytes 36 | } 37 | 38 | fn to_str(&self) -> Result<&str, Utf8Error> { 39 | std::str::from_utf8(self.data()) 40 | } 41 | 42 | /// Splits the name of this symbol into names. See `crate::names::split_names` for details. 43 | pub(crate) fn names(&self) -> Result> { 44 | Ok(NamesIterator::new(DemangleIterator::new(self.to_str()?))) 45 | } 46 | 47 | pub(crate) fn len(&self) -> usize { 48 | self.data().len() 49 | } 50 | 51 | pub(crate) fn module_name(&self) -> Option<&str> { 52 | let mut it = crate::demangle::DemangleIterator::new(self.to_str().ok()?); 53 | if let (Some(DemangleToken::Text(..)), Some(DemangleToken::Text(text))) = 54 | (it.next(), it.next()) 55 | { 56 | Some(text) 57 | } else { 58 | None 59 | } 60 | } 61 | 62 | pub(crate) fn crate_name(&self) -> Option<&str> { 63 | let data_str = self.to_str().ok()?; 64 | if let Some(DemangleToken::Text(text)) = 65 | crate::demangle::DemangleIterator::new(data_str).next() 66 | { 67 | Some(text) 68 | } else { 69 | None 70 | } 71 | } 72 | 73 | /// Returns whether this symbol is one that we should "look through". Such symbols are ones 74 | /// where we pretend they don't exist and treat any outgoing references from the symbol as 75 | /// originating from whatever referenced the look-through symbol. So for example, if 76 | /// foo->core::ops::function::Fn->std::env::var, then we'll consider `foo` as referencing 77 | /// `std::env::var`. 78 | pub(crate) fn is_look_through(&self) -> bool { 79 | let Ok(data) = self.to_str() else { 80 | return false; 81 | }; 82 | let mut tokens = DemangleIterator::new(data); 83 | ["core", "ops", "function"] 84 | .iter() 85 | .all(|p| tokens.next() == Some(DemangleToken::Text(p))) 86 | } 87 | } 88 | 89 | impl Display for Symbol<'_> { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | if let Ok(sym_string) = self.to_str() { 92 | write!(f, "{:#}", demangle(sym_string))?; 93 | } else { 94 | write!(f, "INVALID-UTF-8({:?})", self.data())?; 95 | } 96 | Ok(()) 97 | } 98 | } 99 | 100 | impl Debug for Symbol<'_> { 101 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 | if let Ok(sym_string) = self.to_str() { 103 | // For valid UTF-8, we just print as a string. We want something that fits on one line, 104 | // even when using the alternate format, so that we can efficiently display lists of 105 | // symbols. 106 | Debug::fmt(sym_string, f) 107 | } else { 108 | // For invalid UTF-8, fall back to a default debug formatting. 109 | f.debug_struct("Symbol") 110 | .field("bytes", &self.data()) 111 | .finish() 112 | } 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use super::*; 119 | 120 | fn get_name_vecs<'a>(mut input: NamesIterator<'a, DemangleIterator<'a>>) -> Vec> { 121 | let mut out = Vec::new(); 122 | while let Some((parts, _)) = input.next_name().unwrap() { 123 | let parts: Vec<_> = parts.collect(); 124 | if !parts.is_empty() { 125 | out.push(parts); 126 | } 127 | } 128 | out 129 | } 130 | 131 | #[test] 132 | fn test_names() { 133 | let symbol = Symbol::borrowed(b"_ZN4core3ptr85drop_in_place$LT$std..rt..lang_start$LT$$LP$$RP$$GT$..$u7b$$u7b$closure$u7d$$u7d$$GT$17h0bb7e9fe967fc41cE"); 134 | assert_eq!( 135 | get_name_vecs(symbol.names().unwrap()), 136 | vec![ 137 | vec!["core", "ptr", "drop_in_place"], 138 | vec!["std", "rt", "lang_start"], 139 | ] 140 | ); 141 | 142 | let symbol = Symbol::borrowed( 143 | b"_ZN58_$LT$alloc..string..String$u20$as$u20$core..fmt..Debug$GT$3fmt17h3b29bd412ff2951fE", 144 | ); 145 | assert_eq!( 146 | get_name_vecs(symbol.names().unwrap()), 147 | vec![ 148 | vec!["alloc", "string", "String"], 149 | vec!["core", "fmt", "Debug", "fmt"] 150 | ] 151 | ); 152 | assert_eq!(symbol.module_name(), None); 153 | 154 | assert_eq!(Symbol::borrowed(b"foo").module_name(), None); 155 | } 156 | 157 | #[test] 158 | fn test_names_literal_number() { 159 | let symbol = Symbol::borrowed(b"_ZN104_$LT$proc_macro2..Span$u20$as$u20$syn..span..IntoSpans$LT$$u5b$proc_macro2..Span$u3b$$u20$1$u5d$$GT$$GT$10into_spans17h8cc941d826bfc6f7E"); 160 | assert_eq!( 161 | get_name_vecs(symbol.names().unwrap()), 162 | vec![ 163 | vec!["proc_macro2", "Span"], 164 | vec!["syn", "span", "IntoSpans", "into_spans"], 165 | vec!["proc_macro2", "Span"], 166 | ] 167 | ); 168 | } 169 | 170 | #[test] 171 | fn test_display() { 172 | let symbol = Symbol::borrowed(b"_ZN4core3ptr85drop_in_place$LT$std..rt..lang_start$LT$$LP$$RP$$GT$..$u7b$$u7b$closure$u7d$$u7d$$GT$17h0bb7e9fe967fc41cE"); 173 | assert_eq!( 174 | symbol.to_string(), 175 | "core::ptr::drop_in_place::{{closure}}>" 176 | ); 177 | } 178 | 179 | #[test] 180 | fn comparison() { 181 | fn hash(sym: &Symbol) -> u64 { 182 | let mut hasher = std::collections::hash_map::DefaultHasher::new(); 183 | sym.hash(&mut hasher); 184 | hasher.finish() 185 | } 186 | use std::hash::Hash; 187 | use std::hash::Hasher; 188 | 189 | let sym1 = Symbol::borrowed(b"sym1"); 190 | let sym2 = Symbol::borrowed(b"sym2"); 191 | assert_eq!(sym1, sym1.to_heap()); 192 | assert!(sym1 < sym2); 193 | assert!(sym1.to_heap() < sym2); 194 | assert!(sym1 < sym2.to_heap()); 195 | assert_eq!(hash(&sym1), hash(&sym1.to_heap())); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/symbol_graph/backtrace.rs: -------------------------------------------------------------------------------- 1 | use crate::checker::BinLocation; 2 | use crate::location::SourceLocation; 3 | use anyhow::Context; 4 | use anyhow::Result; 5 | use fxhash::FxHashMap; 6 | use fxhash::FxHashSet; 7 | use gimli::Dwarf; 8 | use std::fmt::Display; 9 | use std::path::Path; 10 | use std::sync::Arc; 11 | 12 | pub(crate) struct Backtracer { 13 | /// A map from symbol addresses in the binary to a list of relocations pointing to that address. 14 | back_references: FxHashMap>, 15 | 16 | bin_bytes: Vec, 17 | 18 | sysroot: Arc, 19 | } 20 | 21 | #[derive(Debug, PartialEq, Eq)] 22 | pub(crate) struct Frame { 23 | pub(crate) name: String, 24 | pub(crate) source_location: Option, 25 | inlined: bool, 26 | } 27 | 28 | impl Backtracer { 29 | pub(crate) fn new(sysroot: Arc) -> Self { 30 | Self { 31 | sysroot, 32 | back_references: Default::default(), 33 | bin_bytes: Default::default(), 34 | } 35 | } 36 | 37 | /// Declare a reference from `bin_location` to `target_address`. 38 | pub(crate) fn add_reference(&mut self, bin_location: BinLocation, target_address: u64) { 39 | self.back_references 40 | .entry(target_address) 41 | .or_default() 42 | .push(bin_location); 43 | } 44 | 45 | pub(crate) fn provide_bin_bytes(&mut self, bin_bytes: Vec) { 46 | self.bin_bytes = bin_bytes; 47 | } 48 | 49 | pub(crate) fn backtrace(&self, bin_location: BinLocation) -> Result> { 50 | let mut addresses = Vec::new(); 51 | self.find_frames( 52 | &mut vec![], 53 | bin_location, 54 | &mut addresses, 55 | &mut FxHashSet::default(), 56 | ); 57 | 58 | let obj = object::File::parse(self.bin_bytes.as_slice()).with_context(|| { 59 | format!( 60 | "Backtrace failed to parse bin file of size {}", 61 | self.bin_bytes.len() 62 | ) 63 | })?; 64 | let owned_dwarf = Dwarf::load(|id| super::load_section(&obj, id))?; 65 | let dwarf = 66 | owned_dwarf.borrow(|section| gimli::EndianSlice::new(section, gimli::LittleEndian)); 67 | let ctx = addr2line::Context::from_dwarf(dwarf) 68 | .context("Failed in addr2line during backtrace")?; 69 | 70 | let mut backtrace: Vec = Vec::new(); 71 | for address in addresses { 72 | let mut frame_iter = ctx.find_frames(address).skip_all_loads()?; 73 | let mut first = true; 74 | while let Some(frame) = frame_iter.next()? { 75 | let name = frame 76 | .function 77 | .and_then(|n| n.name.to_string().ok()) 78 | .map(|n| format!("{:#}", rustc_demangle::demangle(n))) 79 | .unwrap_or_else(|| "??".to_owned()); 80 | let source_location = frame 81 | .location 82 | .and_then(|location| SourceLocation::try_from(&location).ok()) 83 | .map(|location| location.with_sysroot(&self.sysroot)); 84 | if first { 85 | first = false; 86 | } else { 87 | // Mark all frames except the last one as inlined. 88 | backtrace.last_mut().unwrap().inlined = true; 89 | } 90 | backtrace.push(Frame { 91 | name, 92 | source_location, 93 | inlined: false, 94 | }) 95 | } 96 | } 97 | Ok(backtrace) 98 | } 99 | 100 | /// Find the longest sequence of addresses leading to `bin_location`. Why longest? Just a guess 101 | /// that it's likely to be the most interesting. 102 | fn find_frames( 103 | &self, 104 | candidate: &mut Vec, 105 | bin_location: BinLocation, 106 | out: &mut Vec, 107 | visited: &mut FxHashSet, 108 | ) { 109 | if !visited.insert(bin_location.address) { 110 | return; 111 | } 112 | candidate.push(bin_location.address); 113 | if let Some(references) = self.back_references.get(&bin_location.symbol_start) { 114 | for reference in references { 115 | self.find_frames(candidate, *reference, out, visited); 116 | } 117 | } else if candidate.len() > out.len() { 118 | out.resize(candidate.len(), 0); 119 | out.copy_from_slice(candidate); 120 | } 121 | candidate.pop(); 122 | } 123 | } 124 | 125 | impl Display for Frame { 126 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 | self.name.fmt(f)?; 128 | if self.inlined { 129 | " (inlined)".fmt(f)?; 130 | } 131 | Ok(()) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/symbol_graph/object_file_path.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use anyhow::Result; 3 | use std::fmt::Display; 4 | use std::fs::File; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | 8 | /// Represents the name of an object file, possibly contained within an archive. Note, we only 9 | /// support a single level of archive. i.e. archives within archives aren't supported. 10 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 11 | pub(crate) struct ObjectFilePath { 12 | pub(crate) outer: PathBuf, 13 | pub(crate) inner: Option, 14 | } 15 | 16 | impl ObjectFilePath { 17 | pub(crate) fn non_archive(filename: &Path) -> Self { 18 | Self { 19 | outer: filename.to_owned(), 20 | inner: None, 21 | } 22 | } 23 | 24 | pub(crate) fn in_archive(archive: &Path, entry: &ar::Entry) -> Result { 25 | let inner = PathBuf::from( 26 | std::str::from_utf8(entry.header().identifier()).with_context(|| { 27 | format!( 28 | "An archive entry in `{}` is not valid UTF-8", 29 | archive.display() 30 | ) 31 | })?, 32 | ); 33 | Ok(Self { 34 | outer: archive.to_owned(), 35 | inner: Some(inner), 36 | }) 37 | } 38 | } 39 | 40 | impl Display for ObjectFilePath { 41 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | if let Some(inner) = self.inner.as_ref() { 43 | write!(f, "{}[{}]", self.outer.display(), inner.display()) 44 | } else { 45 | write!(f, "{}", self.outer.display()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/timing.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::fmt::Display; 3 | use std::time::Duration; 4 | use std::time::Instant; 5 | 6 | use fxhash::FxHashMap; 7 | 8 | /// Records how long different parts of execution take. 9 | #[derive(Default)] 10 | pub(crate) struct TimingCollector { 11 | enabled: bool, 12 | 13 | /// The order in which each timing category was first reported. We print timings in this order. 14 | order: Vec<&'static str>, 15 | 16 | /// The total time for each category. 17 | timings: FxHashMap<&'static str, Duration>, 18 | } 19 | 20 | impl TimingCollector { 21 | pub(crate) fn new(enabled: bool) -> Self { 22 | Self { 23 | enabled, 24 | order: Vec::new(), 25 | timings: FxHashMap::default(), 26 | } 27 | } 28 | 29 | /// Adds duration since `start` to the timing category `timing`. Returns the time now, which can 30 | /// optionally be used to record the time to the next event. 31 | pub(crate) fn add_timing(&mut self, start: Instant, timing: &'static str) -> Instant { 32 | let now = Instant::now(); 33 | if !self.enabled { 34 | return now; 35 | } 36 | let elapsed = now - start; 37 | match self.timings.entry(timing) { 38 | Entry::Occupied(mut entry) => { 39 | *entry.get_mut() += elapsed; 40 | } 41 | Entry::Vacant(entry) => { 42 | entry.insert(elapsed); 43 | self.order.push(timing); 44 | } 45 | } 46 | now 47 | } 48 | } 49 | 50 | impl Display for TimingCollector { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | for key in &self.order { 53 | writeln!(f, "{key}: {:0.3}s", self.timings[key].as_secs_f32())? 54 | } 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/tmpdir.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::Path; 3 | use std::path::PathBuf; 4 | 5 | pub(crate) enum TempDir { 6 | Owned(tempfile::TempDir), 7 | Borrowed(PathBuf), 8 | } 9 | 10 | impl TempDir { 11 | pub(crate) fn new(path: Option<&Path>) -> Result { 12 | if let Some(path) = path { 13 | Ok(TempDir::Borrowed(path.to_owned())) 14 | } else { 15 | Ok(TempDir::Owned(tempfile::TempDir::new()?)) 16 | } 17 | } 18 | 19 | pub(crate) fn path(&self) -> &Path { 20 | match self { 21 | TempDir::Owned(t) => t.path(), 22 | TempDir::Borrowed(t) => t, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | //! User interface for showing problems to the user and asking them what they'd like to do about 2 | //! them. 3 | 4 | use crate::checker::Checker; 5 | use crate::crate_index::CrateIndex; 6 | use crate::events::AppEvent; 7 | use crate::problem_store::ProblemStoreRef; 8 | use crate::Args; 9 | use anyhow::Result; 10 | use clap::ValueEnum; 11 | use log::info; 12 | use std::path::Path; 13 | use std::sync::mpsc::Receiver; 14 | use std::sync::mpsc::Sender; 15 | use std::sync::Arc; 16 | use std::sync::Mutex; 17 | use std::thread::JoinHandle; 18 | 19 | #[cfg(feature = "ui")] 20 | mod basic_term; 21 | #[cfg(feature = "ui")] 22 | mod full_term; 23 | mod null_ui; 24 | 25 | #[derive(ValueEnum, Debug, Clone, Copy, Default)] 26 | pub(crate) enum Kind { 27 | #[default] 28 | None, 29 | #[cfg(feature = "ui")] 30 | Basic, 31 | #[cfg(feature = "ui")] 32 | Full, 33 | } 34 | 35 | trait UserInterface: Send { 36 | fn run( 37 | &mut self, 38 | problem_store: ProblemStoreRef, 39 | event_receiver: Receiver, 40 | ) -> Result<()>; 41 | } 42 | 43 | pub(crate) fn start_ui( 44 | args: &Arc, 45 | config_path: &Path, 46 | checker: &Arc>, 47 | problem_store: ProblemStoreRef, 48 | crate_index: Arc, 49 | event_receiver: Receiver, 50 | abort_sender: Sender<()>, 51 | ) -> Result>> { 52 | let mut ui: Box = match args.ui_kind() { 53 | Kind::None => { 54 | info!("Starting null UI"); 55 | Box::new(null_ui::NullUi::new(args, abort_sender)) 56 | } 57 | #[cfg(feature = "ui")] 58 | Kind::Basic => { 59 | info!("Starting basic terminal UI"); 60 | Box::new(basic_term::BasicTermUi::new( 61 | config_path.to_owned(), 62 | checker, 63 | )) 64 | } 65 | #[cfg(feature = "ui")] 66 | Kind::Full => { 67 | info!("Starting full terminal UI"); 68 | Box::new(full_term::FullTermUi::new( 69 | config_path.to_owned(), 70 | checker, 71 | crate_index, 72 | abort_sender, 73 | )?) 74 | } 75 | }; 76 | Ok(std::thread::Builder::new() 77 | .name("UI".to_owned()) 78 | .spawn(move || ui.run(problem_store, event_receiver))?) 79 | } 80 | 81 | impl Args { 82 | pub(crate) fn should_capture_cargo_output(&self) -> bool { 83 | !matches!(self.ui_kind(), Kind::None) 84 | } 85 | 86 | fn ui_kind(&self) -> Kind { 87 | if self.no_ui { 88 | return Kind::None; 89 | } 90 | if let Some(kind) = self.ui { 91 | return kind; 92 | } 93 | #[cfg(feature = "ui")] 94 | if std::io::IsTerminal::is_terminal(&std::io::stdout()) { 95 | return Kind::Full; 96 | } 97 | Kind::None 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ui/full_term.rs: -------------------------------------------------------------------------------- 1 | //! A fullscreen terminal user interface. 2 | 3 | use crate::checker::Checker; 4 | use crate::crate_index::CrateIndex; 5 | use crate::events::AppEvent; 6 | use crate::problem_store::ProblemStoreRef; 7 | use anyhow::Result; 8 | use crossterm::event::Event; 9 | use crossterm::event::KeyCode; 10 | use ratatui::backend::CrosstermBackend; 11 | use ratatui::layout::Constraint; 12 | use ratatui::layout::Direction; 13 | use ratatui::layout::Layout; 14 | use ratatui::layout::Rect; 15 | use ratatui::style::Color; 16 | use ratatui::style::Modifier; 17 | use ratatui::style::Style; 18 | use ratatui::widgets::Block; 19 | use ratatui::widgets::BorderType; 20 | use ratatui::widgets::Borders; 21 | use ratatui::widgets::Clear; 22 | use ratatui::widgets::List; 23 | use ratatui::widgets::ListItem; 24 | use ratatui::widgets::ListState; 25 | use ratatui::widgets::Paragraph; 26 | use ratatui::widgets::Wrap; 27 | use ratatui::Frame; 28 | use std::io::Stdout; 29 | use std::path::PathBuf; 30 | use std::sync::mpsc::Receiver; 31 | use std::sync::mpsc::Sender; 32 | use std::sync::mpsc::TryRecvError; 33 | use std::sync::Arc; 34 | use std::sync::Mutex; 35 | use std::time::Duration; 36 | 37 | mod problems_ui; 38 | 39 | pub(crate) struct FullTermUi { 40 | config_path: PathBuf, 41 | abort_sender: Sender<()>, 42 | crate_index: Arc, 43 | checker: Arc>, 44 | } 45 | 46 | impl FullTermUi { 47 | pub(crate) fn new( 48 | config_path: PathBuf, 49 | checker: &Arc>, 50 | crate_index: Arc, 51 | abort_sender: Sender<()>, 52 | ) -> Result { 53 | Ok(Self { 54 | config_path, 55 | abort_sender, 56 | crate_index, 57 | checker: checker.clone(), 58 | }) 59 | } 60 | } 61 | 62 | struct Terminal { 63 | term: ratatui::Terminal>, 64 | // While our UI is active, we hold a lock on stderr. Our output threads try to acquire stderr 65 | // before sending through output from cargo and will thus block output while the UI is active. 66 | _output_lock: std::io::StderrLock<'static>, 67 | } 68 | 69 | impl Terminal { 70 | fn new() -> Result { 71 | crossterm::terminal::enable_raw_mode()?; 72 | let mut stdout = std::io::stdout(); 73 | crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen)?; 74 | let backend = ratatui::backend::CrosstermBackend::new(stdout); 75 | let term = ratatui::Terminal::new(backend)?; 76 | let output_lock = std::io::stderr().lock(); 77 | Ok(Self { 78 | term, 79 | _output_lock: output_lock, 80 | }) 81 | } 82 | } 83 | 84 | impl super::UserInterface for FullTermUi { 85 | fn run( 86 | &mut self, 87 | problem_store: ProblemStoreRef, 88 | event_receiver: Receiver, 89 | ) -> Result<()> { 90 | let mut screen = problems_ui::ProblemsUi::new( 91 | problem_store.clone(), 92 | self.crate_index.clone(), 93 | self.checker.clone(), 94 | self.config_path.clone(), 95 | ); 96 | let mut needs_redraw = true; 97 | let mut error = None; 98 | match event_receiver.recv() { 99 | Ok(AppEvent::ProblemsAdded) => {} 100 | Err(..) | Ok(AppEvent::Shutdown) => return Ok(()), 101 | } 102 | let mut terminal = Terminal::new()?; 103 | loop { 104 | if screen.quit_requested() { 105 | let pstore = &mut problem_store.lock(); 106 | let _ = self.abort_sender.send(()); 107 | // Give cargo a chance to exit before we tell the problem store to abort, otherwise 108 | // cargo might get to see its subprocesses failing which would pollute our output 109 | // with confusing messages. 110 | std::thread::sleep(Duration::from_millis(20)); 111 | pstore.abort(); 112 | // We don't return yet, but rather wait until we get an AppEvent::Shutdown. 113 | } 114 | if needs_redraw { 115 | if screen.needs_cursor() { 116 | terminal.term.show_cursor()?; 117 | } else { 118 | terminal.term.hide_cursor()?; 119 | } 120 | terminal.term.draw(|f| { 121 | screen.render(f); 122 | if let Some(e) = error.as_ref() { 123 | render_error(f, e); 124 | } 125 | })?; 126 | needs_redraw = false; 127 | } 128 | match event_receiver.try_recv() { 129 | Ok(AppEvent::ProblemsAdded) => { 130 | needs_redraw = true; 131 | if let Err(e) = screen.problems_added() { 132 | error = Some(e); 133 | } 134 | } 135 | Ok(AppEvent::Shutdown) => { 136 | return Ok(()); 137 | } 138 | Err(TryRecvError::Disconnected) => return Ok(()), 139 | Err(TryRecvError::Empty) => { 140 | // TODO: Consider spawning a separate thread to read crossterm events, then feed 141 | // them into the main event channel. That way we can avoid polling. 142 | if crossterm::event::poll(Duration::from_millis(100))? { 143 | needs_redraw = true; 144 | let Ok(Event::Key(key)) = crossterm::event::read() else { 145 | continue; 146 | }; 147 | // When we're displaying an error, any key will dismiss the error popup. The key 148 | // should then be ignored. 149 | if error.take().is_some() { 150 | // But still process the quit key, since if the error came from 151 | // rendering, we'd like a way to get out. 152 | if key.code == KeyCode::Char('q') { 153 | problem_store.lock().abort(); 154 | } 155 | continue; 156 | } 157 | if let Err(e) = screen.handle_key(key) { 158 | error = Some(e); 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | impl Drop for Terminal { 168 | fn drop(&mut self) { 169 | let _ = crossterm::terminal::disable_raw_mode(); 170 | let _ = crossterm::execute!( 171 | self.term.backend_mut(), 172 | crossterm::terminal::LeaveAlternateScreen 173 | ); 174 | } 175 | } 176 | 177 | fn render_build_progress(f: &mut Frame, area: Rect) { 178 | let block = Block::default() 179 | .title("Building") 180 | .borders(Borders::ALL) 181 | .border_style(Style::default().fg(Color::Yellow)); 182 | let paragraph = Paragraph::new("Build in progress...") 183 | .block(block) 184 | .wrap(Wrap { trim: false }); 185 | f.render_widget(Clear, area); 186 | f.render_widget(paragraph, area); 187 | } 188 | 189 | fn render_error(f: &mut Frame, error: &anyhow::Error) { 190 | let area = message_area(f.size()); 191 | let block = Block::default() 192 | .title("Error") 193 | .borders(Borders::ALL) 194 | .border_style(Style::default().fg(Color::Red)); 195 | let paragraph = Paragraph::new(format!("{error:#}")) 196 | .block(block) 197 | .wrap(Wrap { trim: false }); 198 | f.render_widget(Clear, area); 199 | f.render_widget(paragraph, area); 200 | } 201 | 202 | fn message_area(area: Rect) -> Rect { 203 | centre_area(area, 80, 25) 204 | } 205 | 206 | fn centre_area(area: Rect, width: u16, height: u16) -> Rect { 207 | let vertical_chunks = Layout::default() 208 | .direction(Direction::Vertical) 209 | .constraints(centre(height, area.height)) 210 | .split(area); 211 | 212 | let horizontal_chunks = Layout::default() 213 | .direction(Direction::Horizontal) 214 | .constraints(centre(width, area.width)) 215 | .split(vertical_chunks[1]); 216 | horizontal_chunks[1] 217 | } 218 | 219 | fn centre(target: u16, available: u16) -> Vec { 220 | let actual = target.min(available); 221 | let margin = (available - actual) / 2; 222 | vec![ 223 | Constraint::Length(margin), 224 | Constraint::Length(actual), 225 | Constraint::Length(margin), 226 | ] 227 | } 228 | 229 | fn render_list( 230 | f: &mut Frame, 231 | title: &str, 232 | items: impl Iterator>, 233 | active: bool, 234 | area: Rect, 235 | index: usize, 236 | ) { 237 | let items: Vec<_> = items.collect(); 238 | let mut block = Block::default().title(title).borders(Borders::ALL); 239 | if active { 240 | block = block 241 | .border_type(BorderType::Thick) 242 | .border_style(Style::default().fg(Color::Yellow)); 243 | } 244 | let mut style = Style::default().add_modifier(Modifier::REVERSED); 245 | if active { 246 | style = style.fg(Color::Yellow); 247 | } 248 | let list = List::new(items).block(block).highlight_style(style); 249 | let mut list_state = ListState::default(); 250 | list_state.select(Some(index)); 251 | f.render_stateful_widget(list, area, &mut list_state); 252 | } 253 | 254 | /// Increment or decrement `counter`, wrapping at `len`. `keycode` must be Down or Up. 255 | fn update_counter(counter: &mut usize, key_code: KeyCode, len: usize) { 256 | match key_code { 257 | KeyCode::Up => *counter = (*counter + len - 1) % len, 258 | KeyCode::Down => *counter = (*counter + len + 1) % len, 259 | _ => panic!("Invalid call to update_counter"), 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/ui/full_term/problems_ui/diff.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Color; 2 | use ratatui::style::Style; 3 | use ratatui::text::Line; 4 | use ratatui::text::Span; 5 | use std::collections::VecDeque; 6 | 7 | /// Builds the styled lines of a diff from `original` to `updated`. Shows common context from the 8 | /// start to the end of the current section. 9 | pub(super) fn diff_lines(original: &str, updated: &str) -> Vec> { 10 | fn is_section_start(line: &str) -> bool { 11 | line.starts_with('[') 12 | } 13 | 14 | let mut lines = Vec::new(); 15 | 16 | let mut common = VecDeque::new(); 17 | let mut after_context = false; 18 | for diff in diff::lines(original, updated) { 19 | match diff { 20 | diff::Result::Both(s, _) => { 21 | if after_context { 22 | if is_section_start(s) { 23 | after_context = false; 24 | } else { 25 | lines.push(Line::from(format!(" {s}"))); 26 | } 27 | } else { 28 | if is_section_start(s) { 29 | common.clear(); 30 | } 31 | common.push_back(s); 32 | } 33 | } 34 | diff::Result::Left(s) => { 35 | for line in common.drain(..) { 36 | lines.push(Line::from(format!(" {line}"))); 37 | } 38 | lines.push(Line::from(vec![Span::styled( 39 | format!("-{s}"), 40 | Style::default().fg(Color::Red), 41 | )])); 42 | after_context = true; 43 | } 44 | diff::Result::Right(s) => { 45 | for line in common.drain(..) { 46 | lines.push(Line::from(format!(" {line}"))); 47 | } 48 | lines.push(Line::from(vec![Span::styled( 49 | format!("+{s}"), 50 | Style::default().fg(Color::Green), 51 | )])); 52 | after_context = true; 53 | } 54 | } 55 | } 56 | lines 57 | } 58 | 59 | /// Attempts, where possible to trim the supplied diff to less than `max_lines`. 60 | pub(super) fn remove_excess_context(lines: &mut Vec, max_lines: usize) { 61 | struct ContextBlock { 62 | start: usize, 63 | length: usize, 64 | to_take: usize, 65 | } 66 | 67 | let mut blocks = Vec::new(); 68 | let mut current_block = None; 69 | for (offset, line) in lines.iter().enumerate() { 70 | let is_context = line 71 | .spans 72 | .first() 73 | .map(|span| span.content.starts_with(' ')) 74 | .unwrap_or(false); 75 | if is_context { 76 | current_block 77 | .get_or_insert(ContextBlock { 78 | start: offset, 79 | length: 0, 80 | to_take: 0, 81 | }) 82 | .length += 1; 83 | } else if let Some(block) = current_block.take() { 84 | blocks.push(block); 85 | } 86 | } 87 | blocks.extend(current_block); 88 | 89 | let mut to_reclaim = (lines.len() as isize).saturating_sub(max_lines as isize); 90 | while to_reclaim > 0 { 91 | if let Some(block) = blocks.iter_mut().max_by_key(|b| b.length - b.to_take) { 92 | if block.length - block.to_take == 0 { 93 | // We've run out of context to remove. 94 | break; 95 | } 96 | if block.to_take > 0 { 97 | to_reclaim -= 1; 98 | } 99 | block.to_take += 1; 100 | } else { 101 | // We have no blocks at all. 102 | break; 103 | } 104 | } 105 | 106 | let old_lines = std::mem::take(lines); 107 | let mut block_index = 0; 108 | blocks.retain(|block| block.to_take > 0); 109 | for (offset, line) in old_lines.into_iter().enumerate() { 110 | let Some(block) = blocks.get(block_index) else { 111 | lines.push(line); 112 | continue; 113 | }; 114 | let skip_start = block.start + (block.length - block.to_take) / 2; 115 | let skip_end = skip_start + block.to_take; 116 | if offset < skip_start { 117 | lines.push(line); 118 | continue; 119 | } 120 | if offset >= skip_end { 121 | lines.push(Line::from("...")); 122 | lines.push(line); 123 | block_index += 1; 124 | } 125 | } 126 | } 127 | 128 | #[test] 129 | fn test_diff_lines() { 130 | fn line_to_string(line: &Line) -> String { 131 | line.spans 132 | .iter() 133 | .map(|span| span.content.as_ref()) 134 | .collect::>() 135 | .join("") 136 | } 137 | let lines = diff_lines( 138 | indoc::indoc! { r#" 139 | a = 1 140 | [section1] 141 | b = 2 142 | x = [ 143 | "x1", 144 | "x2", 145 | "x3", 146 | ] 147 | [section2] 148 | c = 3 149 | d = 4 150 | e = 5 151 | f = 6 152 | g = 7 153 | h = 8 154 | "# }, 155 | indoc::indoc! { r#" 156 | a = 1 157 | [section1] 158 | b = 2 159 | x = [ 160 | "x1", 161 | "x2", 162 | "x3", 163 | ] 164 | [section2] 165 | c = 3 166 | d = 4 167 | e = 5 168 | f = 6 169 | g2 = 7.5 170 | h = 8 171 | "# }, 172 | ); 173 | let lines: Vec<_> = lines.iter().map(line_to_string).collect(); 174 | let expected = vec![ 175 | " [section2]", 176 | " c = 3", 177 | " d = 4", 178 | " e = 5", 179 | " f = 6", 180 | "-g = 7", 181 | "+g2 = 7.5", 182 | " h = 8", 183 | " ", 184 | ]; 185 | assert_eq!(lines, expected); 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use super::*; 191 | use indoc::indoc; 192 | 193 | fn to_lines(text: &str) -> Vec> { 194 | text.lines() 195 | .filter_map(|l| { 196 | // Ignore @ - it's just there to tell indoc where the left margin is. 197 | if !l.starts_with('@') { 198 | Some(Line::from(l.to_owned())) 199 | } else { 200 | None 201 | } 202 | }) 203 | .collect() 204 | } 205 | 206 | fn to_text(lines: &[Line]) -> String { 207 | lines 208 | .iter() 209 | .map(|line| { 210 | line.spans 211 | .iter() 212 | .map(|s| s.content.as_ref()) 213 | .collect::>() 214 | .join("") 215 | }) 216 | .collect::>() 217 | .join("\n") 218 | } 219 | 220 | #[test] 221 | fn test_remove_excess_context() { 222 | let mut lines = to_lines(indoc! {r#" 223 | @ 224 | [common] 225 | -version = 1 226 | +version = 2 227 | a 228 | b 229 | c 230 | d 231 | e 232 | f 233 | g 234 | h 235 | i 236 | j 237 | k 238 | +foo = 1 239 | l 240 | m 241 | n 242 | "#}); 243 | remove_excess_context(&mut lines, 11); 244 | assert_eq!( 245 | to_text(&lines), 246 | to_text(&to_lines(indoc! {r#" 247 | @ 248 | [common] 249 | -version = 1 250 | +version = 2 251 | a 252 | ... 253 | j 254 | k 255 | +foo = 1 256 | l 257 | m 258 | n 259 | "#})) 260 | ); 261 | } 262 | 263 | #[test] 264 | fn test_remove_excess_context_from_empty() { 265 | let mut lines = to_lines(indoc! {r#" 266 | @ 267 | +[common] 268 | +version = 1 269 | +import_std = [ 270 | + "fs", 271 | + "process", 272 | + "net", 273 | +] 274 | "#}); 275 | remove_excess_context(&mut lines, 6); 276 | assert_eq!( 277 | to_text(&lines), 278 | to_text(&to_lines(indoc! {r#" 279 | @ 280 | +[common] 281 | +version = 1 282 | +import_std = [ 283 | + "fs", 284 | + "process", 285 | + "net", 286 | +] 287 | "#})) 288 | ); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/ui/full_term/problems_ui/syntax_styling.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Color; 2 | use rustc_ap_rustc_lexer::LiteralKind; 3 | use rustc_ap_rustc_lexer::TokenKind; 4 | 5 | pub(super) fn colour_for_token_kind(kind: TokenKind, token_text: &str) -> Option { 6 | match kind { 7 | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } => Some(Color::Green), 8 | TokenKind::Ident | TokenKind::RawIdent => { 9 | if is_keyword(token_text) { 10 | Some(Color::Blue) 11 | } else { 12 | Some(Color::LightGreen) 13 | } 14 | } 15 | TokenKind::Literal { 16 | kind: 17 | LiteralKind::Str { .. } 18 | | LiteralKind::ByteStr { .. } 19 | | LiteralKind::RawByteStr { .. } 20 | | LiteralKind::RawStr { .. }, 21 | .. 22 | } => Some(Color::Yellow), 23 | TokenKind::Lifetime { .. } => Some(Color::Blue), 24 | TokenKind::OpenParen | TokenKind::CloseParen => Some(Color::Blue), 25 | TokenKind::OpenBrace | TokenKind::CloseBrace => Some(Color::Magenta), 26 | TokenKind::OpenBracket | TokenKind::CloseBracket => Some(Color::Magenta), 27 | TokenKind::Question => Some(Color::Yellow), 28 | _ => None, 29 | } 30 | } 31 | 32 | const KEYWORDS: &[&str] = &[ 33 | "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", 34 | "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", 35 | "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", 36 | "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", 37 | "override", "priv", "typeof", "unsized", "virtual", "yield", "try", 38 | ]; 39 | 40 | fn is_keyword(token_text: &str) -> bool { 41 | KEYWORDS.contains(&token_text) 42 | } 43 | -------------------------------------------------------------------------------- /src/ui/null_ui.rs: -------------------------------------------------------------------------------- 1 | //! A user-interface that never prompts. This is used when non-interactive mode is selected. 2 | 3 | use crate::events::AppEvent; 4 | use crate::problem::Severity; 5 | use crate::problem_store::ProblemStoreRef; 6 | use crate::Args; 7 | use anyhow::Result; 8 | use colored::Colorize; 9 | use std::sync::mpsc::Receiver; 10 | use std::sync::mpsc::Sender; 11 | use std::sync::Arc; 12 | 13 | pub(crate) struct NullUi { 14 | args: Arc, 15 | abort_sender: Sender<()>, 16 | } 17 | 18 | impl NullUi { 19 | pub(crate) fn new(args: &Arc, abort_sender: Sender<()>) -> Self { 20 | Self { 21 | args: args.clone(), 22 | abort_sender, 23 | } 24 | } 25 | } 26 | 27 | impl super::UserInterface for NullUi { 28 | fn run( 29 | &mut self, 30 | problem_store: ProblemStoreRef, 31 | event_receiver: Receiver, 32 | ) -> Result<()> { 33 | while let Ok(event) = event_receiver.recv() { 34 | match event { 35 | AppEvent::Shutdown => return Ok(()), 36 | AppEvent::ProblemsAdded => { 37 | let mut pstore = problem_store.lock(); 38 | let mut has_errors = false; 39 | for (_, problem) in pstore.deduplicated_into_iter() { 40 | let mut severity = problem.severity(); 41 | if self.args.command.is_some() && severity == Severity::Warning { 42 | // When running for example `cackle test`, not everything will be 43 | // analysed, so unused warnings are expected. As such, we suppress all 44 | // warnings. 45 | continue; 46 | } 47 | if self.args.fail_on_warnings { 48 | severity = Severity::Error 49 | }; 50 | match severity { 51 | Severity::Warning => { 52 | println!("{} {problem:#}", "WARNING:".yellow()) 53 | } 54 | Severity::Error => { 55 | if !has_errors { 56 | has_errors = true; 57 | // Kill cargo process then wait a bit for any terminal output to 58 | // settle before we start reporting errors. 59 | let _ = self.abort_sender.send(()); 60 | std::thread::sleep(std::time::Duration::from_millis(20)); 61 | println!(); 62 | } 63 | println!("{} {problem:#}", "ERROR:".red()) 64 | } 65 | } 66 | } 67 | if has_errors { 68 | pstore.abort(); 69 | } else { 70 | loop { 71 | let maybe_index = pstore 72 | .deduplicated_into_iter() 73 | .next() 74 | .map(|(index, _)| index); 75 | if let Some(index) = maybe_index { 76 | pstore.resolve(index); 77 | } else { 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | Ok(()) 86 | } 87 | } 88 | 89 | #[test] 90 | fn test_null_ui_with_warning() { 91 | use crate::config::permissions::PermSel; 92 | use crate::problem::Problem::UnusedPackageConfig; 93 | 94 | let (abort_sender, _abort_recv) = std::sync::mpsc::channel(); 95 | let mut ui = NullUi::new(&Arc::new(Args::default()), abort_sender); 96 | let (event_send, event_recv) = std::sync::mpsc::channel(); 97 | let mut problem_store = crate::problem_store::create(event_send.clone()); 98 | let join_handle = std::thread::spawn({ 99 | let problem_store = problem_store.clone(); 100 | move || { 101 | crate::ui::UserInterface::run(&mut ui, problem_store, event_recv).unwrap(); 102 | } 103 | }); 104 | let mut problems = crate::problem::ProblemList::default(); 105 | problems.push(UnusedPackageConfig(PermSel::for_primary("crab1"))); 106 | problems.push(UnusedPackageConfig(PermSel::for_primary("crab2"))); 107 | let outcome = problem_store.fix_problems(problems); 108 | assert_eq!(outcome, crate::outcome::Outcome::Continue); 109 | event_send.send(AppEvent::Shutdown).unwrap(); 110 | join_handle.join().unwrap(); 111 | } 112 | -------------------------------------------------------------------------------- /src/unsafe_checker.rs: -------------------------------------------------------------------------------- 1 | //! This module tokenises Rust code and looks for the unsafe keyword. This is done as an additional 2 | //! layer of defence in addition to use of the -Funsafe-code flag when compiling crates, since that 3 | //! flag unfortunately doesn't completely prevent use of unsafe. 4 | 5 | use crate::location::SourceLocation; 6 | use anyhow::Context; 7 | use anyhow::Result; 8 | use std::path::Path; 9 | 10 | /// Returns the locations of all unsafe usages found in `path`. 11 | pub(crate) fn scan_path(path: &Path) -> Result> { 12 | let bytes = 13 | std::fs::read(path).with_context(|| format!("Failed to read `{}`", path.display()))?; 14 | let Ok(source) = std::str::from_utf8(&bytes) else { 15 | // If the file isn't valid UTF-8 then we don't need to check it for the unsafe keyword, 16 | // since it can't be a source file that the rust compiler would accept. 17 | return Ok(Vec::new()); 18 | }; 19 | Ok(scan_string(source, path)) 20 | } 21 | 22 | fn scan_string(source: &str, path: &Path) -> Vec { 23 | let mut offset = 0; 24 | let mut locations = Vec::new(); 25 | for token in rustc_ap_rustc_lexer::tokenize(source) { 26 | let new_offset = offset + token.len; 27 | let token_text = &source[offset..new_offset]; 28 | if token_text == "unsafe" { 29 | let column = source[..new_offset] 30 | .lines() 31 | .last() 32 | .map(|line| (line.len() - token_text.len() + 1) as u32) 33 | .unwrap_or(1); 34 | let line = 1.max(source[..new_offset].lines().count() as u32); 35 | locations.push(SourceLocation::new(path, line, Some(column))); 36 | } 37 | offset = new_offset; 38 | } 39 | locations 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use crate::unsafe_checker::scan_path; 45 | use crate::unsafe_checker::scan_string; 46 | use std::ops::Not; 47 | use std::path::Path; 48 | 49 | fn unsafe_line_col(source: &str) -> Option<(u32, u32)> { 50 | scan_string(source, Path::new("test.rs")) 51 | .first() 52 | .map(|usage| (usage.line(), usage.column().unwrap())) 53 | } 54 | 55 | #[test] 56 | fn test_scan_string() { 57 | assert_eq!(unsafe_line_col("unsafe fn foo() {}"), Some((1, 1))); 58 | assert_eq!( 59 | unsafe_line_col(r#"fn foo() -> &'static str {"unsafe"}"#), 60 | None 61 | ); 62 | assert_eq!(unsafe_line_col("fn foo() { unsafe {} }"), Some((1, 12))); 63 | assert_eq!( 64 | unsafe_line_col(indoc::indoc! {r#" 65 | fn foo() { 66 | unsafe {} 67 | }"# 68 | }), 69 | Some((2, 5)) 70 | ); 71 | assert_eq!( 72 | unsafe_line_col("#[cfg(foo)]\nunsafe fn bar() {}"), 73 | Some((2, 1)) 74 | ); 75 | } 76 | 77 | #[track_caller] 78 | fn has_unsafe_in_file(path: &str) -> bool { 79 | let root = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"); 80 | let root = Path::new(&root); 81 | scan_path(&root.join(path)).unwrap().is_empty().not() 82 | } 83 | 84 | #[test] 85 | fn test_scan_test_crates() { 86 | assert!(has_unsafe_in_file("test_crates/crab-1/src/lib.rs")); 87 | assert!(has_unsafe_in_file("test_crates/crab-1/src/impl1.rs")); 88 | assert!(!has_unsafe_in_file("test_crates/crab-2/src/lib.rs")); 89 | assert!(has_unsafe_in_file("test_crates/crab-3/src/lib.rs")); 90 | assert!(has_unsafe_in_file("test_crates/crab-bin/src/main.rs")); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test_crates/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # For instructions on how to set up cross compiling for Mac from Linux, see 2 | # https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html 3 | 4 | [target.x86_64-apple-darwin] 5 | linker = "x86_64-apple-darwin14-clang" 6 | ar = "x86_64-apple-darwin14-ar" 7 | 8 | -------------------------------------------------------------------------------- /test_crates/.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | custom_target_dir/ 3 | 4 | -------------------------------------------------------------------------------- /test_crates/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "crab-1" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "crab-5", 10 | ] 11 | 12 | [[package]] 13 | name = "crab-10" 14 | version = "0.1.0" 15 | 16 | [[package]] 17 | name = "crab-11" 18 | version = "0.1.0" 19 | 20 | [[package]] 21 | name = "crab-2" 22 | version = "0.1.0" 23 | dependencies = [ 24 | "crab-3 0.1.0", 25 | ] 26 | 27 | [[package]] 28 | name = "crab-3" 29 | version = "0.1.0" 30 | dependencies = [ 31 | "crab-1", 32 | ] 33 | 34 | [[package]] 35 | name = "crab-3" 36 | version = "2.0.0" 37 | 38 | [[package]] 39 | name = "crab-4" 40 | version = "0.1.0" 41 | dependencies = [ 42 | "crab-5", 43 | ] 44 | 45 | [[package]] 46 | name = "crab-5" 47 | version = "0.1.0" 48 | 49 | [[package]] 50 | name = "crab-6" 51 | version = "0.1.0" 52 | dependencies = [ 53 | "crab-5", 54 | ] 55 | 56 | [[package]] 57 | name = "crab-7" 58 | version = "0.1.0" 59 | 60 | [[package]] 61 | name = "crab-8" 62 | version = "0.1.0" 63 | dependencies = [ 64 | "crab-1", 65 | "crab-6", 66 | ] 67 | 68 | [[package]] 69 | name = "crab-9" 70 | version = "0.1.0" 71 | 72 | [[package]] 73 | name = "crab-bin" 74 | version = "0.1.0" 75 | dependencies = [ 76 | "crab-1", 77 | "crab-2", 78 | "crab-3 2.0.0", 79 | "crab-4", 80 | "crab-6", 81 | "crab-7", 82 | "crab-8", 83 | "pmacro-1", 84 | "res-1", 85 | ] 86 | 87 | [[package]] 88 | name = "pmacro-1" 89 | version = "0.1.0" 90 | 91 | [[package]] 92 | name = "res-1" 93 | version = "0.1.0" 94 | dependencies = [ 95 | "crab-6", 96 | ] 97 | 98 | [[package]] 99 | name = "shared-1" 100 | version = "0.1.0" 101 | dependencies = [ 102 | "crab-1", 103 | "crab-2", 104 | ] 105 | -------------------------------------------------------------------------------- /test_crates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crab-1", 4 | "crab-2", 5 | "crab-3", 6 | "crab-4", 7 | "crab-5", 8 | "crab-6", 9 | "crab-7", 10 | "crab-8", 11 | "crab-9", 12 | "crab-10", 13 | "crab-11", 14 | "crab-bin", 15 | "pmacro-1", 16 | "shared-1", 17 | "res-1", 18 | ] 19 | exclude = [ 20 | "crab-3v2", 21 | ] 22 | resolver = "2" 23 | 24 | [profile.cackle-release] 25 | inherits = "release" 26 | opt-level = 0 27 | split-debuginfo = "off" 28 | strip = false 29 | debug = 2 30 | lto = "off" 31 | -------------------------------------------------------------------------------- /test_crates/a.txt: -------------------------------------------------------------------------------- 1 | Hello -------------------------------------------------------------------------------- /test_crates/cackle.toml: -------------------------------------------------------------------------------- 1 | [common] 2 | version = 2 3 | features = [ 4 | "crash-if-not-sandboxed", 5 | "foo", 6 | ] 7 | import_std = [ 8 | "fs", 9 | "env", 10 | "net", 11 | "process", 12 | "unix_sockets", 13 | "terminate", 14 | ] 15 | 16 | [sandbox] 17 | kind = "Bubblewrap" 18 | 19 | # Restrict access to an entire crate. 20 | [api.res1] 21 | include = [ 22 | "res_1", 23 | ] 24 | 25 | [api.fs] 26 | no_auto_detect = [ 27 | "crab-3", 28 | ] 29 | 30 | [api.restrict1] 31 | include = [ 32 | "crab_1::restrict1", 33 | ] 34 | 35 | [api.terminate] 36 | include = [ 37 | "crab-3::terminate", 38 | ] 39 | 40 | [pkg.crab-1] 41 | allow_unsafe = true 42 | import = [ 43 | "fs", 44 | ] 45 | allow_apis = [ 46 | # Don't include fs permission. 47 | "env", 48 | "terminate", 49 | ] 50 | build.allow_apis = [ 51 | "net", 52 | ] 53 | build.sandbox.allow_network = true 54 | from.test.allow_apis = [ 55 | "unix_sockets", 56 | ] 57 | 58 | [pkg.crab-2] 59 | allow_apis = [ 60 | "env", 61 | "fs", 62 | ] 63 | build.allow_apis = [ 64 | "process", 65 | ] 66 | build.allow_build_instructions = [ 67 | "cargo:rustc-env=*", 68 | "cargo:rustc-link-*" 69 | ] 70 | build.allow_unsafe = true 71 | 72 | [pkg.crab-3] 73 | allow_unsafe = true 74 | allow_apis = [ 75 | "crab-1::fs", 76 | # Code in a file that's generated by build.rs. 77 | "env", 78 | "process", 79 | "restrict1", 80 | "terminate", 81 | ] 82 | build.allow_apis = [ 83 | "fs", 84 | ] 85 | 86 | [pkg.pmacro-1] 87 | allow_apis = [ 88 | "env", 89 | "fs", 90 | ] 91 | allow_proc_macro = true 92 | 93 | [pkg.crab-bin] 94 | allow_apis = [ 95 | "env", 96 | "fs", 97 | "res1", 98 | ] 99 | allow_unsafe = true 100 | build.sandbox.kind = "Disabled" 101 | 102 | [pkg.crab-4] 103 | allow_apis = [ 104 | "env", 105 | "fs", 106 | "process", 107 | ] 108 | allow_unsafe = true 109 | 110 | [pkg.crab-5] 111 | allow_apis = [ 112 | ] 113 | from.build.allow_apis = [ 114 | "fs", 115 | ] 116 | 117 | [pkg.shared-1] 118 | allow_unsafe = true 119 | allow_apis = [ 120 | "crab-1::fs", 121 | "env", 122 | ] 123 | 124 | [pkg.crab-7] 125 | allow_unsafe = true 126 | allow_apis = [ 127 | "env", 128 | "fs", 129 | ] 130 | 131 | [pkg.crab-8] 132 | allow_apis = [ 133 | "fs", 134 | ] 135 | 136 | [pkg.crab-9] 137 | allow_apis = [ 138 | "env", 139 | ] 140 | test.sandbox.bind_writable = [ 141 | "crab-9/scratch" 142 | ] 143 | test.sandbox.pass_env = [ 144 | "CRAB_9_CRASH_TEST", 145 | ] 146 | test.allow_apis = [ 147 | "env", 148 | "fs", 149 | "process", 150 | ] 151 | 152 | [pkg.crab-10] 153 | allow_unsafe = true 154 | build.allow_apis = [ 155 | "env", 156 | "fs", 157 | "process", 158 | ] 159 | allow_build_instructions = [ 160 | "cargo:rustc-link-*", 161 | ] 162 | 163 | [pkg.crab-11] 164 | test.sandbox.kind = "Disabled" 165 | test.allow_apis = [ 166 | "env", 167 | "fs", 168 | "terminate", 169 | ] 170 | -------------------------------------------------------------------------------- /test_crates/crab-1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | crab-5 = { path = "../crab-5" } 10 | -------------------------------------------------------------------------------- /test_crates/crab-1/build.rs: -------------------------------------------------------------------------------- 1 | use std::net::ToSocketAddrs; 2 | 3 | fn main() { 4 | if option_env!("CACKLE_TEST_NO_NET").is_none() { 5 | "rust-lang.org:443" 6 | .to_socket_addrs() 7 | .expect("Failed to resolve rust-lang.org and CACKLE_TEST_NO_NET is not set"); 8 | } 9 | assert!(crab_5::do_something()); 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/crab-1/cackle/export.toml: -------------------------------------------------------------------------------- 1 | [common] 2 | version = 1 3 | 4 | [api.fs] 5 | include = [ 6 | "crab_1::read_file", 7 | ] 8 | -------------------------------------------------------------------------------- /test_crates/crab-1/src/impl1.rs: -------------------------------------------------------------------------------- 1 | #[inline(never)] 2 | pub fn crab_1(v: u32) -> u32 { 3 | #[allow(clippy::transmute_num_to_bytes)] 4 | unsafe { 5 | let mut v2: [u8; 4] = core::mem::transmute(v); 6 | v2.reverse(); 7 | println!("hello from crab_1"); 8 | core::mem::transmute(v2) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/crab-1/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | mod impl1; 4 | 5 | pub use crate::impl1::crab_1; 6 | 7 | /// This struct causes hashbrown to be linked in, which, if we're not careful we can have trouble 8 | /// identifying the source location for. 9 | #[derive(Debug)] 10 | pub struct HashMapWrapper { 11 | pub foo: HashMap, 12 | } 13 | 14 | /// This function is declared as performing filesystem access by cackle.toml. We also call it 15 | /// ourselves, but we don't want functions that we define and call to count as permissions that 16 | /// we're using. 17 | pub fn read_file(_path: &str) -> Option { 18 | None 19 | } 20 | 21 | pub fn call_read_file() { 22 | read_file("tmp.txt"); 23 | } 24 | 25 | /// Binds a TCP port. This function is dead code, so this should not be considered. 26 | pub fn do_network_stuff() { 27 | std::net::TcpListener::bind("127.0.0.1:9876").unwrap(); 28 | } 29 | 30 | /// This function shows up in the dynamic symbols of shared1, so should count as used. 31 | #[no_mangle] 32 | pub extern "C" fn crab_1_entry() { 33 | println!("{:?}", std::env::var("HOME")); 34 | } 35 | 36 | /// Makes sure that we attribute this call to abort to this crate, not the crate that calls this 37 | /// function, even though it's marked as inline(always). 38 | #[inline(always)] 39 | pub fn inlined_abort() { 40 | std::process::abort(); 41 | } 42 | 43 | /// This function is only called from a test in crab_3. 44 | pub fn do_unix_socket_stuff() { 45 | let _ = std::os::unix::net::UnixStream::pair(); 46 | } 47 | 48 | /// A function that we restrict access to, is inlined and which calls no other functions. This tests 49 | /// that inlined function usages are attributed correctly. 50 | #[inline(always)] 51 | pub fn restrict1() -> u64 { 52 | let mut x: u64; 53 | unsafe { 54 | std::arch::asm!( 55 | "mov {res}, 42", 56 | res = out(reg) x); 57 | } 58 | x 59 | } 60 | -------------------------------------------------------------------------------- /test_crates/crab-10/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-10" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /test_crates/crab-10/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::process::Command; 3 | 4 | fn main() { 5 | let base_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 6 | let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); 7 | let object_file = out_dir.join("cpp_stuff.o"); 8 | run(Command::new("c++") 9 | .arg("-g") 10 | .arg("-c") 11 | .arg(base_dir.join("cpp_stuff.cc")) 12 | .arg("-o") 13 | .arg(&object_file)); 14 | run(Command::new("ar") 15 | .arg("r") 16 | .arg(out_dir.join("libcpp_stuff.a")) 17 | .arg(&object_file)); 18 | println!("cargo:rustc-link-search={}", out_dir.display()); 19 | println!("cargo:rustc-link-lib=cpp_stuff",); 20 | println!("cargo:rerun-if-changed=cpp_stuff.cc"); 21 | let v = [42, 43]; 22 | assert_eq!(*unsafe { v.get_unchecked(0) }, 42); 23 | assert_eq!(*unsafe { v.get_unchecked(1) }, 43); 24 | } 25 | 26 | fn run(cmd: &mut Command) { 27 | match cmd.status() { 28 | Ok(status) => { 29 | if status.code() != Some(0) { 30 | panic!("Command exited with non-zero status while running:\n{cmd:?}"); 31 | } 32 | } 33 | Err(_) => { 34 | panic!("Failed to run {cmd:?}"); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test_crates/crab-10/cpp_stuff.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace foo { 4 | namespace bar { 5 | namespace { 6 | 7 | int32_t get_value() { return 42; } 8 | 9 | } // namespace 10 | } // namespace bar 11 | } // namespace foo 12 | 13 | extern "C" { 14 | 15 | int32_t cpp_entry_point() { 16 | return foo::bar::get_value(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /test_crates/crab-10/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | fn cpp_entry_point() -> i32; 3 | } 4 | 5 | pub fn call_cpp_code() -> i32 { 6 | unsafe { cpp_entry_point() } 7 | } 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use crate::call_cpp_code; 12 | 13 | #[test] 14 | fn test_call_cpp_code() { 15 | assert_eq!(call_cpp_code(), 42); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test_crates/crab-11/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-11" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /test_crates/crab-11/scratch/test-output.txt: -------------------------------------------------------------------------------- 1 | crab_11 test output -------------------------------------------------------------------------------- /test_crates/crab-11/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate has a test that tries to write to a file in the source directory. This should succeed 2 | //! because the sandbox is disabled for this crate. 3 | 4 | use std::path::Path; 5 | 6 | pub fn access_file() { 7 | let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 8 | let output_path = manifest_dir.join("scratch").join("test-output.txt"); 9 | std::fs::write(output_path, "crab_11 test output").unwrap(); 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | 16 | #[test] 17 | fn it_works() { 18 | access_file(); 19 | 20 | // Verify that APIs used from the test itself are attributed as we expect. 21 | if std::env::var("CACKLE_TEST_TERMINATE_1").is_ok() { 22 | std::process::abort(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test_crates/crab-2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-2" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | crab-3 = { path = "../crab-3" } 8 | 9 | [features] 10 | crash-if-not-sandboxed = ["crab-3/crash-if-not-sandboxed"] 11 | -------------------------------------------------------------------------------- /test_crates/crab-2/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::process::Command; 3 | 4 | fn main() { 5 | let base_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 6 | let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); 7 | let object_file = out_dir.join("nothing.o"); 8 | run(Command::new("cc") 9 | .arg("-c") 10 | .arg(base_dir.join("nothing.c")) 11 | .arg("-o") 12 | .arg(&object_file)); 13 | run(Command::new("ar") 14 | .arg("r") 15 | .arg(out_dir.join("libnothing.a")) 16 | .arg(&object_file)); 17 | println!("cargo:rustc-link-search={}", out_dir.display()); 18 | println!("cargo:rerun-if-changed=nothing.c"); 19 | println!("cargo:rustc-env=CRAB_2_ENV=42"); 20 | let v = [42, 43]; 21 | assert_eq!(*unsafe { v.get_unchecked(0) }, 42); 22 | assert_eq!(*unsafe { v.get_unchecked(1) }, 43); 23 | } 24 | 25 | fn run(cmd: &mut Command) { 26 | match cmd.status() { 27 | Ok(status) => { 28 | if status.code() != Some(0) { 29 | panic!("Command exited with non-zero status while running:\n{cmd:?}"); 30 | } 31 | } 32 | Err(_) => { 33 | panic!("Failed to run {cmd:?}"); 34 | } 35 | } 36 | } 37 | 38 | // This crate already uses unsafe in regular code above. Here we define a macro that uses unsafe. 39 | // This isn't checked by any tests, but is useful for manual testing that we're merging both sources 40 | // of unsafe. 41 | #[macro_export] 42 | macro_rules! check_something { 43 | () => { 44 | let v = [42, 43]; 45 | assert_eq!(*unsafe { v.get_unchecked(0) }, 42); 46 | }; 47 | } 48 | 49 | // This unsafe usage won't be picked up by the token checker, but will be picked up by the compiler. 50 | // This is for manual testing of how this displays in the UI. 51 | #[no_mangle] 52 | pub fn this_is_unsafe_too() {} 53 | -------------------------------------------------------------------------------- /test_crates/crab-2/nothing.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void __attribute__ ((constructor)) premain() { 4 | printf("nothing to see here\n"); 5 | } 6 | -------------------------------------------------------------------------------- /test_crates/crab-2/src/bin/c2-bin.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if std::env::var("CRASH_IF_DEFINED").is_ok() { 3 | panic!(); 4 | } 5 | let total: i32 = std::env::args() 6 | .skip(1) 7 | .map(|arg| -> i32 { arg.parse().unwrap_or_default() }) 8 | .sum(); 9 | println!("{total}"); 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/crab-2/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Make sure that we don't add any unsafe to this crate. We want this crate to have no unsafe since 2 | // it has an include_bytes! below and we want to make sure that include_bytes! of invalid UTF-8 3 | // doesn't cause the unsafe token-based checker any problems. If we had some actual unsafe in this 4 | // crate, then we'd need to allow it and then the unsafe-checker wouldn't run. 5 | #![forbid(unsafe_code)] 6 | 7 | use bar::write as woozle; 8 | use fob::fs as bar; 9 | use std as fob; 10 | 11 | const DATA: &[u8] = include_bytes!("../../data/random.data"); 12 | 13 | pub mod stuff { 14 | #[link( 15 | name = "nothing", 16 | kind = "static", 17 | modifiers = "-bundle,+whole-archive" 18 | )] 19 | extern "C" {} 20 | 21 | pub fn do_stuff() { 22 | let crab_2_env = env!("CRAB_2_ENV"); 23 | assert_eq!(crab_2_env, "42"); 24 | 25 | crab_3::macro_that_uses_unsafe!({ 26 | crate::woozle("/tmp/foo.bar", [42]).unwrap(); 27 | true 28 | }); 29 | crab_3::do_stuff(); 30 | 31 | // This is here as a way to allow cackle's integration test to trigger a rebuild of this 32 | // file (by changing the environment variable). 33 | println!("CRAB_2_EXT_ENV: {:?}", option_env!("CRAB_2_EXT_ENV")); 34 | 35 | let total: u32 = crate::DATA.iter().cloned().map(|byte| byte as u32).sum(); 36 | println!("{}", total); 37 | } 38 | } 39 | 40 | #[inline(always)] 41 | pub fn res_b() -> u64 { 42 | crab_3::res_a() 43 | } 44 | -------------------------------------------------------------------------------- /test_crates/crab-3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-3" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build/main.rs" 6 | 7 | [dependencies] 8 | crab-1 = { path = "../crab-1" } 9 | 10 | [features] 11 | crash-if-not-sandboxed = [] 12 | -------------------------------------------------------------------------------- /test_crates/crab-3/build/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | write_output_files(); 6 | 7 | // Generally RUSTC_WRAPPER shouldn't be set when we run our build script, but if it is, the path 8 | // it points to should exist. 9 | if let Ok(rustc_wrapper) = std::env::var("RUSTC_WRAPPER") { 10 | if !Path::new(&rustc_wrapper).exists() { 11 | panic!("RUSTC_WRAPPER is set to {rustc_wrapper}, but that doesn't exist"); 12 | } 13 | } 14 | } 15 | 16 | fn write_output_files() { 17 | let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); 18 | std::fs::write(out_dir.join("extra_code.rs"), r#"std::env::var("PATH")"#).unwrap(); 19 | let home = PathBuf::from(std::env::var("HOME").unwrap()); 20 | 21 | if !cfg!(feature = "crash-if-not-sandboxed") { 22 | return; 23 | } 24 | 25 | // This file shouldn't exist in the sandbox, even if it exists outside it. 26 | let credentials_path = home.join(".cargo/credentials"); 27 | if std::fs::read(&credentials_path).is_ok() { 28 | panic!( 29 | "We shouldn't be able to read {}", 30 | credentials_path.display() 31 | ); 32 | } 33 | 34 | // We shouldn't be able to write to the cargo registry. 35 | let registry = home.join(".cargo/registry"); 36 | if !registry.exists() { 37 | panic!("{} should exist", registry.display()); 38 | } 39 | let file_to_write = registry.join("cannot-write-here.txt"); 40 | if std::fs::write(&file_to_write, "test").is_ok() { 41 | std::fs::remove_file(&file_to_write).unwrap(); 42 | panic!("We shouldn't be able to write {}", file_to_write.display()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test_crates/crab-3/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "Cargo.toml", 4 | "test_crates/Cargo.toml" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test_crates/crab-3/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! foo { 3 | () => { 4 | std::process::exit(1) 5 | }; 6 | } 7 | 8 | #[macro_export] 9 | macro_rules! macro_that_uses_unsafe { 10 | ($a:expr) => { 11 | let v = $a; 12 | let mut x = 0_u32; 13 | if v { 14 | x = unsafe { core::mem::transmute(-10_i32) }; 15 | } 16 | x 17 | }; 18 | } 19 | 20 | pub fn do_stuff() { 21 | let path = include!(concat!(env!("OUT_DIR"), "/extra_code.rs")); 22 | println!("{path:?}"); 23 | crab_1::read_file(""); 24 | fs::do_stuff(); 25 | terminate::do_stuff(); 26 | foo!(); 27 | } 28 | 29 | #[test] 30 | fn test_do_unix_socket_stuff() { 31 | crab_1::do_unix_socket_stuff(); 32 | } 33 | 34 | /// We don't actually do any filesystem-related stuff here, but we provide a module named "fs" to 35 | /// confirm that we detect this as a possible exported API. 36 | pub mod fs { 37 | pub(super) fn do_stuff() {} 38 | } 39 | 40 | /// As for the fs module. We need two modules, one to ignore and one to classify as an API. 41 | pub mod terminate { 42 | pub(super) fn do_stuff() {} 43 | } 44 | 45 | #[inline(always)] 46 | pub fn res_a() -> u64 { 47 | crab_1::restrict1() 48 | } 49 | -------------------------------------------------------------------------------- /test_crates/crab-3v2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-3" 3 | version = "2.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /test_crates/crab-3v2/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn run_process() { 2 | let _x = std::process::Command::new("echo").output(); 3 | } 4 | -------------------------------------------------------------------------------- /test_crates/crab-4/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-4" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Has multiple uses of an API" 6 | documentation = "Not published, so has no docs" 7 | 8 | [dependencies] 9 | 10 | [build-dependencies] 11 | crab-5 = { path = "../crab-5" } 12 | -------------------------------------------------------------------------------- /test_crates/crab-4/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | assert!(crab_5::do_something()); 3 | 4 | // Check a selection of the environment variables that cargo sets and which we should pass 5 | // through to build scripts. 6 | let variables = ["OPT_LEVEL", "PROFILE", "OUT_DIR", "CARGO", "TARGET", "HOST"]; 7 | for var in variables { 8 | if let Err(std::env::VarError::NotPresent) = std::env::var(var) { 9 | panic!("Environment variable `{var}` not set in build script") 10 | } 11 | } 12 | 13 | // Verify that we can't access the socket used to communicate with the main cackle process. 14 | if let Some(socket_path) = option_env!("CACKLE_SOCKET_PATH").map(std::path::Path::new) { 15 | if socket_path.exists() { 16 | panic!( 17 | "socket_path: `{}` accessible from build script sandbox", 18 | socket_path.display() 19 | ); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test_crates/crab-4/src/impl1.rs: -------------------------------------------------------------------------------- 1 | #[inline(never)] 2 | pub fn crab_1(v: u32) -> u32 { 3 | #[allow(clippy::transmute_num_to_bytes)] 4 | unsafe { 5 | let mut v2: [u8; 4] = core::mem::transmute(v); 6 | v2.reverse(); 7 | println!("hello from crab_1"); 8 | core::mem::transmute(v2) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/crab-4/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate helps test having multiple uses of the same API. It also tests that we can get 2 | //! results from this crate in parallel to crab_1, since neither depends on the other. 3 | 4 | use std::ffi::OsString; 5 | use std::path::Path; 6 | 7 | pub fn access_file() { 8 | f1(); 9 | f2(); 10 | f3(); 11 | f4(); 12 | let mut s = std::mem::ManuallyDrop::new("Hello".to_owned()); 13 | let s2 = unsafe { String::from_raw_parts(s.as_mut_ptr(), s.len(), s.capacity()) }; 14 | println!("{s2}"); 15 | } 16 | 17 | fn f1() { 18 | let _ = Path::new("a.txt").exists(); 19 | } 20 | 21 | fn f2() { 22 | let _ = Path::new("a.txt").exists(); 23 | } 24 | 25 | fn f3() { 26 | let _ = Path::new("a.txt").exists(); 27 | } 28 | 29 | fn f4() { 30 | let _ = Path::new("a.txt").exists(); 31 | } 32 | 33 | /// Make sure that we can't circumvent checks by accessing a function via a function pointer instead 34 | /// of a direct function call. 35 | static GET_ENV: &[&(dyn (Fn(&'static str) -> Option) + Sync + 'static)] = 36 | &[&std::env::var_os::<&'static str>]; 37 | 38 | pub fn get_home() -> Option { 39 | (GET_ENV[0])("HOME") 40 | } 41 | 42 | /// Same thing as `GET_ENV`, but make sure it works across crate boundaries. 43 | pub static GET_PID: &[&(dyn (Fn() -> u32) + Sync + 'static)] = &[&std::process::id]; 44 | -------------------------------------------------------------------------------- /test_crates/crab-5/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-5" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /test_crates/crab-5/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate is significant in that two separate build.rs files depend on it. This means that any 2 | //! problems encountered while checking this crate might show up twice, since the two build.rs files 3 | //! can be checked in parallel. 4 | 5 | use std::path::Path; 6 | use std::sync::atomic::AtomicU8; 7 | 8 | pub fn do_something() -> bool { 9 | helper() 10 | } 11 | 12 | fn helper() -> bool { 13 | let c = || Path::new("/").exists(); 14 | c() 15 | } 16 | 17 | pub struct Metadata; 18 | 19 | pub struct MacroCallsite { 20 | _interest: AtomicU8, 21 | meta: &'static Metadata, 22 | } 23 | 24 | impl MacroCallsite { 25 | pub const fn new(meta: &'static Metadata) -> Self { 26 | Self { 27 | _interest: AtomicU8::new(0), 28 | meta, 29 | } 30 | } 31 | 32 | #[inline(always)] 33 | pub fn metadata(&self) -> &Metadata { 34 | self.meta 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test_crates/crab-6/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-6" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | crab-5 = { path = "../crab-5" } 8 | -------------------------------------------------------------------------------- /test_crates/crab-6/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate makes no use of any classified APIs. 2 | 3 | use std::ops::Deref; 4 | 5 | pub use crab_5::MacroCallsite; 6 | pub use crab_5::Metadata; 7 | 8 | pub fn add(left: u32, right: u32) -> u32 { 9 | left + right 10 | } 11 | 12 | pub fn print_default() { 13 | println!("default: {:?}", T::default()); 14 | // Make use of an associated type that isn't part of our function signature's generics. 15 | let _ = T::default().deref(); 16 | } 17 | 18 | pub trait Foo { 19 | fn foo(&self); 20 | fn foo2(&self); 21 | } 22 | 23 | #[macro_export] 24 | macro_rules! impl_foo { 25 | ($name:ident) => { 26 | pub struct $name; 27 | 28 | impl $crate::Foo for $name { 29 | fn foo(&self) {} 30 | fn foo2(&self) { 31 | self.foo(); 32 | } 33 | } 34 | }; 35 | } 36 | 37 | /// This macro, together with some code in crab_5, reproduces a minimal subset of a structure present 38 | /// in the tracing/tracing-core crates. If the debug macro is invoked from another crate, say res1, 39 | /// then we observe a reference `crab_5::MacroCallsite::metadata -> res1::print_something::CALLSITE`. 40 | /// If `res1` is a restricted API, then crab_5 is flagged as using that restricted API, even though 41 | /// it cannot, since crab_5 doesn't depend on res1. 42 | #[macro_export] 43 | macro_rules! debug { 44 | () => {{ 45 | static META: $crate::Metadata = $crate::Metadata; 46 | static CALLSITE: $crate::MacroCallsite = $crate::MacroCallsite::new(&META); 47 | let meta = CALLSITE.metadata(); 48 | }}; 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | 55 | #[test] 56 | fn it_works() { 57 | let result = add(2, 2); 58 | assert_eq!(result, 4); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test_crates/crab-7/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-7" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /test_crates/crab-7/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | pub fn do_something() {} 4 | 5 | /// This function runs before main. Make sure that we detect that it uses filesystem APIs. 6 | extern "C" fn before_main() { 7 | // We avoid actually calling filesystem APIs before main, since Rust doesn't really support 8 | // pre-main activities and with recent versions doing so causes an assertion failure. We can 9 | // apparently still get away with the environment variable check. 10 | if std::env::var("FOO").is_ok() { 11 | println!("Does / exist?: {:?}", Path::new("/").exists()); 12 | } 13 | } 14 | 15 | #[link_section = ".init_array"] 16 | #[used] 17 | static INIT_ARRAY: [extern "C" fn(); 1] = [before_main]; 18 | -------------------------------------------------------------------------------- /test_crates/crab-8/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-8" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | crab-6 = { path = "../crab-6" } 8 | 9 | [build-dependencies] 10 | crab-1 = { path = "../crab-1" } 11 | -------------------------------------------------------------------------------- /test_crates/crab-8/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | maybe_abort(false); 3 | } 4 | 5 | // This function is private, so doesn't go into th symbol table. This can make it harder for us to 6 | // find the source locations for references from this function. 7 | fn maybe_abort(abort: bool) { 8 | if abort { 9 | crab_1::inlined_abort(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test_crates/crab-8/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn print_defaults() { 2 | // Instantiate cra6::print_default with a PathBuf. This shouldn't count as crab_6 using the fs 3 | // API, but it should count as this crate using it. 4 | crab_6::print_default::(); 5 | } 6 | -------------------------------------------------------------------------------- /test_crates/crab-9/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-9" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /test_crates/crab-9/scratch/writable.txt: -------------------------------------------------------------------------------- 1 | This file is written by a test -------------------------------------------------------------------------------- /test_crates/crab-9/src/bin/crab9-bin.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::os::unix::prelude::OsStrExt; 3 | 4 | fn main() { 5 | if std::env::args_os().nth(1).as_deref() == Some(OsStr::from_bytes(&[0xff])) { 6 | println!("42"); 7 | } else { 8 | println!("Didn't get expected arguments. Got: {:?}", std::env::args()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/crab-9/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate's test checks various properties of the sandbox that it's running in. 2 | 3 | use std::path::Path; 4 | 5 | pub fn access_files() { 6 | let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 7 | let output_path = manifest_dir.join("crab_9-test-output.txt"); 8 | let contents = format!("{} {} {}", "DO", "NOT", "SUBMIT"); 9 | 10 | // This write should fail because the sandbox should prevent writes to the source directory. 11 | if std::fs::write(&output_path, contents).is_ok() { 12 | panic!( 13 | "crab_9 test was run without a sandbox. Wrote {}", 14 | output_path.display() 15 | ); 16 | } 17 | 18 | // This write should succeed because the sandbox configuration for this package allows writing 19 | // to the scratch directory. 20 | let output_path = manifest_dir.join("scratch/writable.txt"); 21 | if let Err(error) = std::fs::write(&output_path, "This file is written by a test") { 22 | panic!("Failed to write {}: {error}", output_path.display()); 23 | } 24 | 25 | // Verify that we can't access the socket used to communicate with main cackle process. 26 | // CACKLE_SOCKET_PATH should always be set when we build via cackle, so we fail the test if it 27 | // wasn't set. That means this test can only be run via cackle. We do however want the test to 28 | // be able to build outside of cackle, so we use option_env! rather than env! to make it a 29 | // runtime error not a build-time error. 30 | let socket_path = option_env!("CACKLE_SOCKET_PATH") 31 | .map(Path::new) 32 | .expect("CACKLE_SOCKET_PATH not set at build time"); 33 | if socket_path.exists() { 34 | panic!( 35 | "socket_path: `{}` accessible from test sandbox", 36 | socket_path.display() 37 | ); 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn it_works() { 47 | access_files(); 48 | } 49 | 50 | #[test] 51 | fn conditional_crash() { 52 | if std::env::var("CRAB_9_CRASH_TEST").is_ok() { 53 | panic!("Deliberate crash"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test_crates/crab-9/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::os::unix::prelude::OsStrExt; 3 | use std::path::PathBuf; 4 | 5 | #[test] 6 | fn run_crab_9_bin() { 7 | let program = target_dir().join("crab9-bin"); 8 | let mut command = std::process::Command::new(&program); 9 | 10 | // Make sure that we can pass non-UTF-8 arguments to our binary. This can break if we let cackle 11 | // try to parse the arguments with clap. 12 | command.arg(OsStr::from_bytes(&[0xff])); 13 | 14 | match command.output() { 15 | Ok(output) => { 16 | let stdout = &String::from_utf8(output.stdout).unwrap(); 17 | let stderr = &String::from_utf8(output.stderr).unwrap(); 18 | if stdout.trim() != "42" { 19 | println!("=== stdout ===\n{stdout}\n=== stderr ===\n{stderr}"); 20 | panic!("Unexpected output"); 21 | } 22 | } 23 | Err(error) => panic!("Failed to run {}: {}", program.display(), error), 24 | } 25 | } 26 | 27 | fn target_dir() -> PathBuf { 28 | std::env::current_exe() 29 | .unwrap() 30 | .parent() 31 | .unwrap() 32 | .parent() 33 | .unwrap() 34 | .to_owned() 35 | } 36 | -------------------------------------------------------------------------------- /test_crates/crab-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crab-bin" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | crab-1 = { path = "../crab-1" } 8 | crab-2 = { path = "../crab-2" } 9 | crab-3 = { path = "../crab-3v2" } 10 | crab-4 = { path = "../crab-4" } 11 | crab-6 = { path = "../crab-6" } 12 | crab-7 = { path = "../crab-7" } 13 | crab-8 = { path = "../crab-8" } 14 | pmacro-1 = { path = "../pmacro-1" } 15 | res-1 = { path = "../res-1" } 16 | 17 | [features] 18 | crash-if-not-sandboxed = [ 19 | "crab-2/crash-if-not-sandboxed", 20 | "pmacro-1/crash-if-not-sandboxed", 21 | ] 22 | foo = [] 23 | -------------------------------------------------------------------------------- /test_crates/crab-bin/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | std::fs::write( 3 | "scratch/written-by-build-script.txt", 4 | "Hello from crab-bin.build", 5 | ) 6 | .unwrap(); 7 | 8 | // Tell cargo that it doesn't need to rerun this build script on every build. 9 | println!("cargo:rerun-if-changed=build.rs"); 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/crab-bin/scratch/written-by-build-script.txt: -------------------------------------------------------------------------------- 1 | Hello from crab-bin.build -------------------------------------------------------------------------------- /test_crates/crab-bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use pmacro_1::baz; 2 | use pmacro_1::FooBar; 3 | 4 | pmacro_1::create_write_to_file!(); 5 | 6 | pub trait FooBar { 7 | fn foo_bar() -> u32; 8 | } 9 | 10 | #[derive(FooBar)] 11 | struct Foo {} 12 | 13 | fn main() { 14 | let values = [1, 2, crab_6::add(40, 2)]; 15 | // This unsafe is here to make sure that we handle unsafe code in packages with hyphens in their 16 | // name correctly. This is easy to mess up since the crate name passed to rustc will have an 17 | // underscore instead of a hyphen. 18 | let value = crab_1::crab_1(*unsafe { values.get_unchecked(2) }); 19 | println!("{value}"); 20 | non_mangled_function(); 21 | println!("HOME: {:?}", crab_4::get_home()); 22 | write_to_file("a.txt", "Hello"); 23 | println!("pid={}", (crab_4::GET_PID[0])()); 24 | crab_4::access_file(); 25 | crab_7::do_something(); 26 | crab_8::print_defaults(); 27 | crab_3::run_process(); 28 | res_1::print_something(); 29 | assert_eq!(crab_2::res_b(), 42); 30 | assert_eq!(Foo::foo_bar(), 42); 31 | assert_eq!(function_with_custom_attr(), 40); 32 | // Note, the following call exits 33 | crab_2::stuff::do_stuff(); 34 | } 35 | 36 | #[baz] 37 | fn function_with_custom_attr() -> i32 { 38 | 40 39 | } 40 | 41 | #[no_mangle] 42 | fn non_mangled_function() { 43 | // Make sure we don't miss function references from non-mangled functions. 44 | println!("{:?}", std::env::var("HOME")); 45 | if std::env::var("SET_THIS_TO_ABORT").is_ok() { 46 | crab_1::inlined_abort(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test_crates/crab-bin/src/not-utf8.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cackle-rs/cackle/87d0fa148466e41af58b3303c3d3df8bbcaed650/test_crates/crab-bin/src/not-utf8.data -------------------------------------------------------------------------------- /test_crates/data/random.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cackle-rs/cackle/87d0fa148466e41af58b3303c3d3df8bbcaed650/test_crates/data/random.data -------------------------------------------------------------------------------- /test_crates/pmacro-1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pmacro-1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | [features] 11 | crash-if-not-sandboxed = [] 12 | -------------------------------------------------------------------------------- /test_crates/pmacro-1/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use proc_macro::TokenStream; 3 | 4 | #[proc_macro] 5 | pub fn create_write_to_file(_item: TokenStream) -> TokenStream { 6 | if cfg!(feature = "crash-if-not-sandboxed") 7 | && std::fs::write("a.txt", "pmacro-1 wrote this").is_ok() 8 | { 9 | panic!("pmacro-1 running without sandbox (was able to write a.txt)"); 10 | } 11 | if std::env::var("PWD").as_deref() == Ok("/foo/bar") { 12 | println!("This seems unlikely"); 13 | } 14 | r#"fn write_to_file(path: &str, text: &str) { 15 | std::fs::write(path, text).unwrap(); 16 | }"# 17 | .parse() 18 | .unwrap() 19 | } 20 | 21 | #[proc_macro_derive(FooBar, attributes(marker))] 22 | pub fn derive_foo_bar(_item: TokenStream) -> TokenStream { 23 | "impl FooBar for Foo { fn foo_bar() -> u32 { 42 } }" 24 | .parse() 25 | .unwrap() 26 | } 27 | 28 | #[proc_macro_attribute] 29 | pub fn baz(_attr: TokenStream, item: TokenStream) -> TokenStream { 30 | item 31 | } 32 | -------------------------------------------------------------------------------- /test_crates/res-1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "res-1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | crab-6 = { path = "../crab-6" } 8 | -------------------------------------------------------------------------------- /test_crates/res-1/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crab_6::impl_foo; 2 | use crab_6::Foo; 3 | 4 | impl_foo!(Res); 5 | 6 | pub fn print_something() { 7 | let r = Res {}; 8 | r.foo2(); 9 | crab_6::debug!(); 10 | } 11 | -------------------------------------------------------------------------------- /test_crates/shared-1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared-1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = [ "cdylib" ] 8 | 9 | [dependencies] 10 | crab-1 = { path = "../crab-1" } 11 | crab-2 = { path = "../crab-2" } 12 | -------------------------------------------------------------------------------- /test_crates/shared-1/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[no_mangle] 2 | pub extern "C" fn shared1_entry1() { 3 | let v = ["a.txt"]; 4 | crab_1::read_file(v[0]); 5 | } 6 | 7 | #[no_mangle] 8 | pub extern "C" fn shared1_entry2() { 9 | println!("{:?}", std::env::var("HOME")); 10 | crab_2::stuff::do_stuff(); 11 | } 12 | 13 | #[allow(dead_code)] 14 | pub fn an_unused_function() { 15 | // Since this function isn't used, the use of the "process" API should be ignored. Even though 16 | // it's marked as public, it should be discarded when linking the shared object. 17 | std::process::Command::new("ls").output().unwrap(); 18 | } 19 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use anyhow::Result; 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | use std::process::Command; 6 | use tempfile::TempDir; 7 | 8 | #[test] 9 | fn integration_test() -> Result<()> { 10 | fn run_with_args(tmpdir: &TempDir, args: &[&str], expect_failure: bool) -> Result { 11 | let mut command = Command::new(cackle_exe()); 12 | // Remove cargo and rust-related environment variables. In particular we want to remove 13 | // variables that cargo sets, but which won't always be set. For example CARGO_PKG_NAME is 14 | // set by cargo when it invokes rustc, but only when it's compiling a package, not when it 15 | // queries rustc for version information. If we allow such variables to pass through, then 16 | // our code that proxies rustc can appear to work from the test, but only because the test 17 | // itself was run from cargo. 18 | for (var, _) in std::env::vars() { 19 | if var.starts_with("CARGO") || var.starts_with("RUST") { 20 | command.env_remove(var); 21 | } 22 | } 23 | // Delete everything from our tmpdir. Deleting files makes sure that we don't mask bugs that 24 | // would only occur with a fresh temporary directory. We don't delete the directory itself 25 | // because recreating it with the same name could be a security issue on a shared system. 26 | for entry in tmpdir.path().read_dir()? { 27 | let entry = entry?; 28 | if entry.path().is_dir() { 29 | std::fs::remove_dir_all(entry.path()) 30 | } else { 31 | std::fs::remove_file(entry.path()) 32 | } 33 | .with_context(|| format!("Failed to remove `{}`", entry.path().display()))?; 34 | } 35 | let root = crate_root().join("test_crates"); 36 | let output = command 37 | .env("CARGO_TARGET_DIR", "custom_target_dir") 38 | .arg("acl") 39 | .arg("--fail-on-warnings") 40 | .arg("--save-requests") 41 | .arg("--path") 42 | .arg(&root) 43 | // Use the same tmpdir for all our runs. This speeds up this test because many of our 44 | // tests depend on CACKLE_SOCKET_PATH, so would otherwise need to be rebuilt whenever it 45 | // changes. 46 | .arg("--tmpdir") 47 | .arg(tmpdir.path()) 48 | .arg("--ui=none") 49 | .args(args) 50 | .stdout(std::process::Stdio::piped()) 51 | .stderr(std::process::Stdio::inherit()) 52 | .output() 53 | .with_context(|| format!("Failed to invoke `{}`", cackle_exe().display()))?; 54 | 55 | let stdout = std::str::from_utf8(&output.stdout).unwrap().to_owned(); 56 | let stderr = std::str::from_utf8(&output.stderr).unwrap().to_owned(); 57 | if expect_failure { 58 | if output.status.success() { 59 | panic!("Test succeeded when we expected it to fail. Output:\n{stdout}\n{stderr}"); 60 | } 61 | } else if !output.status.success() { 62 | panic!("Test failed when we expected it to succeed. Output:\n{stdout}\n{stderr}"); 63 | } 64 | Ok(stdout) 65 | } 66 | 67 | let tmpdir = TempDir::new()?; 68 | 69 | run_with_args(&tmpdir, &[], false)?; 70 | 71 | // Trigger crab-2 to rebuild its test, but not rerun its build script. This ensures that 72 | // variables set by the build script survive between runs even if the build script doesn't 73 | // rerun. 74 | std::env::set_var("CRAB_2_EXT_ENV", "1"); 75 | 76 | run_with_args(&tmpdir, &["test", "-v"], false)?; 77 | let out = run_with_args( 78 | &tmpdir, 79 | &["run", "--bin", "c2-bin", "--", "40", "4", "-2"], 80 | false, 81 | )?; 82 | let out = out.trim(); 83 | let n: i32 = match out.parse() { 84 | Ok(x) => x, 85 | Err(_) => panic!("Unexpected output. Expected integer, got `{out}`"), 86 | }; 87 | assert_eq!(n, 42); 88 | 89 | std::env::set_var("CRAB_9_CRASH_TEST", "1"); 90 | let out = run_with_args( 91 | &tmpdir, 92 | &[ 93 | "--features", 94 | "", 95 | "test", 96 | "-p", 97 | "crab-9", 98 | "conditional_crash", 99 | ], 100 | true, 101 | )?; 102 | if !out.contains("Deliberate crash") { 103 | panic!("Test failed, but didn't contain expected message. Output was:\n{out}"); 104 | } 105 | 106 | Ok(()) 107 | } 108 | 109 | /// Makes sure that if we supply an invalid toml file, that the error message includes details of 110 | /// the problem. 111 | #[test] 112 | fn invalid_config() -> Result<()> { 113 | let tmpdir = tempfile::tempdir()?; 114 | let dir = tmpdir.path().join("foo"); 115 | create_cargo_dir(&dir); 116 | let config_path = dir.join("cackle.toml"); 117 | std::fs::write(config_path, "invalid_key = true")?; 118 | let output = Command::new(cackle_exe()) 119 | .arg("acl") 120 | .arg("--path") 121 | .arg(dir) 122 | .arg("--ui=none") 123 | .output() 124 | .with_context(|| format!("Failed to invoke `{}`", cackle_exe().display()))?; 125 | assert!(!output.status.success()); 126 | let stdout = std::str::from_utf8(&output.stdout).unwrap(); 127 | let stderr = std::str::from_utf8(&output.stderr).unwrap(); 128 | if !stdout.contains("invalid_key") { 129 | println!("=== stdout ===\n{stdout}\n=== stderr ===\n{stderr}"); 130 | panic!("Error doesn't mention invalid_key"); 131 | } 132 | Ok(()) 133 | } 134 | 135 | fn create_cargo_dir(dir: &Path) { 136 | Command::new("cargo") 137 | .arg("new") 138 | .arg("--vcs") 139 | .arg("none") 140 | .arg("--offline") 141 | .arg(dir) 142 | .status() 143 | .expect("Failed to run `cargo new`"); 144 | } 145 | 146 | fn cackle_exe() -> PathBuf { 147 | target_dir().join("cargo-acl") 148 | } 149 | 150 | fn crate_root() -> PathBuf { 151 | PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()) 152 | } 153 | 154 | fn target_dir() -> PathBuf { 155 | std::env::current_exe() 156 | .unwrap() 157 | .parent() 158 | .unwrap() 159 | .parent() 160 | .unwrap() 161 | .to_owned() 162 | } 163 | --------------------------------------------------------------------------------