kani_verifier/
lib.rs

1// Copyright Kani Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! This crate includes two "proxy binaries": `kani` and `cargo-kani`.
5//! These are conveniences to make it easy to:
6//!
7//! ```bash
8//! cargo install --locked kani-verifer
9//! ```
10//!
11//! Upon first run, or upon running `cargo-kani setup`, these proxy
12//! binaries will download the appropriate Kani release bundle and invoke
13//! the "real" `kani` and `cargo-kani` binaries.
14
15mod cmd;
16mod os_hacks;
17mod setup;
18
19use std::ffi::OsString;
20use std::os::unix::prelude::CommandExt;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use std::{env, fs};
24
25use anyhow::{Context, Result, bail};
26
27/// Effectively the entry point (i.e. `main` function) for both our proxy binaries.
28/// `bin` should be either `kani` or `cargo-kani`
29pub fn proxy(bin: &str) -> Result<()> {
30    match parse_args(env::args_os().collect()) {
31        ArgsResult::ExplicitSetup { use_local_bundle, use_local_toolchain } => {
32            setup::setup(use_local_bundle, use_local_toolchain)
33        }
34        ArgsResult::Default => {
35            fail_if_in_dev_environment()?;
36            if !setup::appears_setup() {
37                setup::setup(None, None)?;
38            } else {
39                // This handles cases where the setup was left incomplete due to an interrupt
40                // For example - https://github.com/model-checking/kani/issues/1545
41                if let Some(path_to_bundle) = setup::appears_incomplete() {
42                    setup::setup(Some(path_to_bundle.clone().into_os_string()), None)?;
43                    // Suppress warning with unused assignment
44                    // and remove the bundle if it still exists
45                    let _ = fs::remove_file(path_to_bundle);
46                }
47            }
48            exec(bin)
49        }
50    }
51}
52
53/// Minimalist argument parsing result type
54#[derive(PartialEq, Eq, Debug)]
55enum ArgsResult {
56    ExplicitSetup { use_local_bundle: Option<OsString>, use_local_toolchain: Option<OsString> },
57    Default,
58}
59
60/// Parse `args` and decide what to do.
61fn parse_args(args: Vec<OsString>) -> ArgsResult {
62    // In an effort to keep our dependencies minimal, we do the bare minimum argument parsing manually.
63    // `args_ez` makes it easy to do crude arg parsing with match.
64    let args_ez: Vec<Option<&str>> = args.iter().map(|x| x.to_str()).collect();
65    // "cargo kani setup" comes in as "cargo-kani kani setup"
66    // "cargo-kani setup" comes in as "cargo-kani setup"
67    match &args_ez[..] {
68        &[_, Some("setup"), Some("--use-local-bundle"), _, Some("--use-local-toolchain"), _] => {
69            ArgsResult::ExplicitSetup {
70                use_local_bundle: Some(args[3].clone()),
71                use_local_toolchain: Some(args[5].clone()),
72            }
73        }
74        &[
75            _,
76            Some("kani"),
77            Some("setup"),
78            Some("--use-local-bundle"),
79            _,
80            Some("--use-local-toolchain"),
81            _,
82        ] => ArgsResult::ExplicitSetup {
83            use_local_bundle: Some(args[4].clone()),
84            use_local_toolchain: Some(args[6].clone()),
85        },
86        &[_, Some("setup"), Some("--use-local-bundle"), _] => ArgsResult::ExplicitSetup {
87            use_local_bundle: Some(args[3].clone()),
88            use_local_toolchain: None,
89        },
90        &[_, Some("kani"), Some("setup"), Some("--use-local-bundle"), _] => {
91            ArgsResult::ExplicitSetup {
92                use_local_bundle: Some(args[4].clone()),
93                use_local_toolchain: None,
94            }
95        }
96        &[_, Some("setup"), Some("--use-local-toolchain"), _] => ArgsResult::ExplicitSetup {
97            use_local_bundle: None,
98            use_local_toolchain: Some(args[3].clone()),
99        },
100        &[_, Some("kani"), Some("setup"), Some("--use-local-toolchain"), _] => {
101            ArgsResult::ExplicitSetup {
102                use_local_bundle: None,
103                use_local_toolchain: Some(args[4].clone()),
104            }
105        }
106        &[_, Some("setup")] | &[_, Some("kani"), Some("setup")] => {
107            ArgsResult::ExplicitSetup { use_local_bundle: None, use_local_toolchain: None }
108        }
109        _ => ArgsResult::Default,
110    }
111}
112
113/// In dev environments, this proxy shouldn't be used.
114/// But accidentally using it (with the test suite) can fire off
115/// hundreds of HTTP requests trying to download a non-existent release bundle.
116/// So if we positively detect a dev environment, raise an error early.
117fn fail_if_in_dev_environment() -> Result<()> {
118    // Don't collapse if as let_chains have only been stabilized in 1.88, which the target host
119    // installing Kani need not have just yet.
120    #[allow(clippy::collapsible_if)]
121    if let Ok(exe) = std::env::current_exe() {
122        if let Some(path) = exe.parent() {
123            if path.ends_with("target/debug") || path.ends_with("target/release") {
124                bail!(
125                    "Running a release-only executable, {}, from a development environment. This is usually caused by PATH including 'target/release' erroneously.",
126                    exe.file_name().unwrap().to_string_lossy()
127                )
128            }
129        }
130    }
131
132    Ok(())
133}
134
135/// Executes `kani-driver` in `bin` mode (kani or cargo-kani)
136/// augmenting environment variables to accomodate our release environment
137fn exec(bin: &str) -> Result<()> {
138    let kani_dir = setup::kani_dir()?;
139    let program = kani_dir.join("bin").join("kani-driver");
140    let pyroot = kani_dir.join("pyroot");
141    let bin_kani = kani_dir.join("bin");
142    let bin_pyroot = pyroot.join("bin");
143
144    // Allow python scripts to find dependencies under our pyroot
145    let pythonpath = prepend_search_path(&[pyroot], env::var_os("PYTHONPATH"))?;
146    // Add: kani, cbmc, and our rust toolchain directly to our PATH
147    let path = prepend_search_path(&[bin_kani, bin_pyroot], env::var_os("PATH"))?;
148
149    // Ensure our environment variables for linker search paths won't cause failures, before we execute:
150    fixup_dynamic_linking_environment();
151    // Override our `RUSTUP_TOOLCHAIN` with the version Kani links against
152    set_kani_rust_toolchain(&kani_dir)?;
153
154    let mut cmd = Command::new(program);
155    cmd.args(env::args_os().skip(1)).env("PYTHONPATH", pythonpath).env("PATH", path).arg0(bin);
156
157    let result = cmd.status().context("Failed to invoke kani-driver")?;
158
159    std::process::exit(result.code().expect("No exit code?"));
160}
161
162/// Prepend paths to an environment variable search string like PATH
163fn prepend_search_path(paths: &[PathBuf], original: Option<OsString>) -> Result<OsString> {
164    match original {
165        None => Ok(env::join_paths(paths)?),
166        Some(original) => {
167            let orig = env::split_paths(&original);
168            let new_iter = paths.iter().cloned().chain(orig);
169            Ok(env::join_paths(new_iter)?)
170        }
171    }
172}
173
174/// `rustup` sets dynamic linker paths when it proxies to the target Rust toolchain. It's not fully
175/// clear why. `rustup run` exists, which may aid in running Rust binaries that dynamically link to
176/// the Rust standard library with `-C prefer-dynamic`. This might be why. All toolchain binaries
177/// have `RUNPATH` set, so it's not needed by e.g. rustc. (Same for Kani)
178///
179/// However, this causes problems for us when the default Rust toolchain is nightly. Then
180/// `LD_LIBRARY_PATH` is set to a nightly `lib` that may contain a different version of
181/// `librustc_driver-*.so` that might have the same name. This takes priority over the `RUNPATH` of
182/// `kani-compiler` and causes the linker to use a slightly different version of rustc than Kani
183/// was built against. This manifests in errors like:
184/// `kani-compiler: symbol lookup error: ... undefined symbol`
185///
186/// Consequently, let's remove from our linking environment anything that looks like a toolchain
187/// path that rustup set. Then we can safely invoke our binaries. Note also that we update
188/// `PATH` in [`exec`] to include our favored Rust toolchain, so we won't re-drive `rustup` when
189/// `kani-driver` later invokes `cargo`.
190fn fixup_dynamic_linking_environment() {
191    #[cfg(not(target_os = "macos"))]
192    const LOADER_PATH: &str = "LD_LIBRARY_PATH";
193    #[cfg(target_os = "macos")]
194    const LOADER_PATH: &str = "DYLD_FALLBACK_LIBRARY_PATH";
195
196    if let Some(paths) = env::var_os(LOADER_PATH) {
197        // unwrap safety: we're just filtering, so it should always succeed
198        let new_val =
199            env::join_paths(env::split_paths(&paths).filter(unlike_toolchain_path)).unwrap();
200        unsafe {
201            env::set_var(LOADER_PATH, new_val);
202        }
203    }
204}
205
206/// Determines if a path looks unlike a toolchain library path. These often looks like:
207/// `/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib`
208// Ignore this lint (recommending Path instead of PathBuf),
209// we want to take the right argument type for use in `filter` above.
210#[allow(clippy::ptr_arg)]
211fn unlike_toolchain_path(path: &PathBuf) -> bool {
212    let mut components = path.iter().rev();
213
214    // effectively matching `*/toolchains/*/lib`
215    !(components.next() == Some(std::ffi::OsStr::new("lib"))
216        && components.next().is_some()
217        && components.next() == Some(std::ffi::OsStr::new("toolchains")))
218}
219
220/// We should currently see a `RUSTUP_TOOLCHAIN` that was set by whatever default
221/// toolchain the user has. We override our own environment variable (that is passed
222/// down to children) with the toolchain Kani uses instead.
223fn set_kani_rust_toolchain(kani_dir: &Path) -> Result<()> {
224    let toolchain_verison = setup::get_rust_toolchain_version(kani_dir)?;
225    unsafe {
226        env::set_var("RUSTUP_TOOLCHAIN", toolchain_verison);
227    }
228    Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn check_unlike_toolchain_path() {
237        fn trial(s: &str) -> bool {
238            unlike_toolchain_path(&PathBuf::from(s))
239        }
240        // filter these out:
241        assert!(!trial("/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib"));
242        assert!(!trial("/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/"));
243        assert!(!trial("/home/user/.rustup/toolchains/nightly/lib"));
244        assert!(!trial("/home/user/.rustup/toolchains/stable/lib"));
245        // minimally:
246        assert!(!trial("toolchains/nightly/lib"));
247        // keep these:
248        assert!(trial("/home/user/.rustup/toolchains"));
249        assert!(trial("/usr/lib"));
250        assert!(trial("/home/user/lib/toolchains"));
251        // don't error on these:
252        assert!(trial(""));
253        assert!(trial("/"));
254    }
255
256    #[test]
257    fn check_arg_parsing() {
258        fn trial(args: &[&str]) -> ArgsResult {
259            parse_args(args.iter().map(OsString::from).collect())
260        }
261        {
262            let e = ArgsResult::Default;
263            assert_eq!(e, trial(&["cargo-kani", "kani"]));
264            assert_eq!(e, trial(&[]));
265        }
266        {
267            let e = ArgsResult::ExplicitSetup { use_local_bundle: None, use_local_toolchain: None };
268            assert_eq!(e, trial(&["cargo-kani", "kani", "setup"]));
269            assert_eq!(e, trial(&["cargo", "kani", "setup"]));
270            assert_eq!(e, trial(&["cargo-kani", "setup"]));
271        }
272        {
273            let e = ArgsResult::ExplicitSetup {
274                use_local_bundle: Some(OsString::from("FILE")),
275                use_local_toolchain: None,
276            };
277            assert_eq!(e, trial(&["cargo-kani", "kani", "setup", "--use-local-bundle", "FILE"]));
278            assert_eq!(e, trial(&["cargo", "kani", "setup", "--use-local-bundle", "FILE"]));
279            assert_eq!(e, trial(&["cargo-kani", "setup", "--use-local-bundle", "FILE"]));
280        }
281        {
282            let e = ArgsResult::ExplicitSetup {
283                use_local_bundle: None,
284                use_local_toolchain: Some(OsString::from("TOOLCHAIN")),
285            };
286            assert_eq!(
287                e,
288                trial(&["cargo-kani", "kani", "setup", "--use-local-toolchain", "TOOLCHAIN"])
289            );
290            assert_eq!(e, trial(&["cargo", "kani", "setup", "--use-local-toolchain", "TOOLCHAIN"]));
291            assert_eq!(e, trial(&["cargo-kani", "setup", "--use-local-toolchain", "TOOLCHAIN"]));
292        }
293        {
294            let e = ArgsResult::ExplicitSetup {
295                use_local_bundle: Some(OsString::from("FILE")),
296                use_local_toolchain: Some(OsString::from("TOOLCHAIN")),
297            };
298            assert_eq!(
299                e,
300                trial(&[
301                    "cargo-kani",
302                    "kani",
303                    "setup",
304                    "--use-local-bundle",
305                    "FILE",
306                    "--use-local-toolchain",
307                    "TOOLCHAIN"
308                ])
309            );
310            assert_eq!(
311                e,
312                trial(&[
313                    "cargo",
314                    "kani",
315                    "setup",
316                    "--use-local-bundle",
317                    "FILE",
318                    "--use-local-toolchain",
319                    "TOOLCHAIN"
320                ])
321            );
322            assert_eq!(
323                e,
324                trial(&[
325                    "cargo-kani",
326                    "setup",
327                    "--use-local-bundle",
328                    "FILE",
329                    "--use-local-toolchain",
330                    "TOOLCHAIN"
331                ])
332            );
333        }
334    }
335}