diff --git a/crates/mind-body/clippy.toml b/crates/mind-body/clippy.toml new file mode 100644 index 0000000..e8c28d0 --- /dev/null +++ b/crates/mind-body/clippy.toml @@ -0,0 +1,21 @@ +# The brake's root is a constant: no function, argument, variable or flag lets a +# caller choose it (control.rs). The strongest form of "no variable" is that no +# code in this crate can read the process's environment at all, whatever name it +# would read, so the rule is a lint on the readers and not a list of names. +# `#![forbid(clippy::disallowed_methods)]` in lib.rs keeps the library from +# allowing them back; the binary (main.rs) and the one integration test that +# spawns itself (tests/control.rs) carry the only allows, each with its reason. +disallowed-methods = [ + { path = "std::env::var", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, + { path = "std::env::var_os", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, + { path = "std::env::vars", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, + { path = "std::env::vars_os", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, + { path = "std::env::set_var", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, + { path = "std::env::remove_var", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, + { path = "std::env::args", reason = "the command line is caller-chosen input; only main.rs reads it" }, + { path = "std::env::args_os", reason = "the command line is caller-chosen input; only main.rs reads it" }, + { path = "std::env::current_exe", reason = "the executable's location is caller-chosen; the brake's root is CONTROL_ROOT" }, + { path = "std::env::temp_dir", reason = "reads TMPDIR; the brake's root is CONTROL_ROOT" }, + { path = "std::env::home_dir", reason = "reads HOME; the brake's root is CONTROL_ROOT" }, + { path = "libc::getenv", reason = "the process environment is caller-chosen input; the brake's root is CONTROL_ROOT" }, +] diff --git a/crates/mind-body/src/lib.rs b/crates/mind-body/src/lib.rs index 4ebad12..8cf4565 100644 --- a/crates/mind-body/src/lib.rs +++ b/crates/mind-body/src/lib.rs @@ -9,6 +9,11 @@ //! repetition breaker, the one-live-Self-run rule, and the single path that //! opens a run in the mind and only then starts its unit. +// The control root is a constant, so the library reads no part of the process +// environment (clippy.toml lists the readers); `forbid` keeps an `allow` from +// putting one back. +#![forbid(clippy::disallowed_methods)] + pub mod cli; pub mod control; pub mod launch; diff --git a/crates/mind-body/src/main.rs b/crates/mind-body/src/main.rs index 2b620fe..014faf2 100644 --- a/crates/mind-body/src/main.rs +++ b/crates/mind-body/src/main.rs @@ -19,6 +19,9 @@ fn restrict_umask() { #[cfg(not(unix))] fn restrict_umask() {} +// The binary is the one place that reads the process: its arguments and the +// operator's name. The library reads neither (clippy.toml, `#![forbid]` in lib.rs). +#[allow(clippy::disallowed_methods, reason = "the binary reads its own arguments and the operator's name, and hands them to the library")] fn main() { restrict_umask(); let args: Vec = std::env::args().skip(1).collect(); diff --git a/crates/mind-body/tests/control.rs b/crates/mind-body/tests/control.rs index 88e309e..6c9c159 100644 --- a/crates/mind-body/tests/control.rs +++ b/crates/mind-body/tests/control.rs @@ -9,6 +9,11 @@ //! as root (the verify container does); the tests that need a second user become //! uid 65534 with setpriv and skip with a message when they cannot. +// The suite re-executes its own test binary as the writer children, and passes +// each child its role and paths through the environment. That is test plumbing in +// its own crate: it reaches no library code, which `#![forbid]`s these readers. +#![allow(clippy::disallowed_methods, reason = "the suite re-executes itself and hands each child its role through the environment")] + use std::path::{Path, PathBuf}; use std::time::Duration; diff --git a/crates/mind-body/tests/forge.rs b/crates/mind-body/tests/forge.rs index d2ff7d4..ee36fe0 100644 --- a/crates/mind-body/tests/forge.rs +++ b/crates/mind-body/tests/forge.rs @@ -1,12 +1,13 @@ //! The brake-grant rule seen from outside the crate: the only reader takes the //! instance, so whatever a caller builds elsewhere (a store in a temporary //! directory, a symlink to another instance's directory, a copy of another -//! instance's store, the relative root `.`, a tree that every environment -//! variable points at) is never what a Grant is made from. -//! The signature half of the rule (no root argument, no source reader, no -//! struct literal, no test constructor) is the `compile_fail` examples on -//! `read_effective`; this is the behavioural half, committed because it is the -//! only defence of the rule against a forged store. +//! instance's store, the relative root `.`) is never what a Grant is made from. +//! The rest of the rule is held where it is decided, not here: the library's +//! public surface is pinned as a whole (tests/public_surface.rs: no new public +//! way to set a root or build a Grant), the process environment and command line +//! are unreadable by library code (clippy.toml, `#![forbid]` in lib.rs), and the +//! `compile_fail` examples on `read_effective` show the signature half. This is +//! the behavioural half against a forged store. #![cfg(unix)] @@ -64,60 +65,4 @@ fn no_forged_store_yields_a_grant() { let relative = reads(FORGED); std::env::set_current_dir(before).unwrap(); assert!(matches!(relative, Effective::Held { .. }), "relative root"); - - // A root named by the environment. The forged tree is laid out for a value - // used as the root itself and for one with `minds` joined on, and every - // variable the process has, with the names an implementation would likely - // read, is pointed at it. A variable of a name neither list holds is not - // reached; the library build having no `std::env` reader is the rest of it. - let environed = tempfile::tempdir().unwrap(); - release(environed.path(), FORGED); - std::fs::create_dir_all(environed.path().join("minds")).unwrap(); - release(&environed.path().join("minds"), FORGED); - let value = environed.path().as_os_str().to_owned(); - let mut names: Vec = std::env::vars_os().map(|(name, _)| name).collect(); - names.extend(GUESSED_ROOT_VARIABLES.iter().map(Into::into)); - let saved: Vec<_> = names.iter().map(|name| (name.clone(), std::env::var_os(name))).collect(); - // SAFETY: this file's one test is the only thread touching the environment. - unsafe { - for name in &names { - std::env::set_var(name, &value); - } - } - let through_environment = reads(FORGED); - unsafe { - for (name, before) in saved { - match before { - Some(before) => std::env::set_var(name, before), - None => std::env::remove_var(name), - } - } - } - assert!(matches!(through_environment, Effective::Held { .. }), "root from an environment variable"); } - -/// Names a caller-chosen root would plausibly be read from. -const GUESSED_ROOT_VARIABLES: [&str; 22] = [ - "MIND_CONTROL_ROOT", - "MIND_BODY_ROOT", - "MIND_ROOT", - "MINDS_ROOT", - "MINDS_DIR", - "CONTROL_ROOT", - "CONTROL_DIR", - "GAMECULT_MINDS", - "GAMECULT_ROOT", - "HUGINN_ROOT", - "HUGINN_HOME", - "EUREKA_ROOT", - "EUREKA_HOME", - "EUREKA_INSTANCE_ROOT", - "STATE_DIRECTORY", - "CONFIGURATION_DIRECTORY", - "RUNTIME_DIRECTORY", - "XDG_CONFIG_HOME", - "XDG_STATE_HOME", - "XDG_DATA_HOME", - "HOME", - "TMPDIR", -];