mind-body: the process environment is unreadable by library code, by lint

clippy.toml lists the std::env readers (var, var_os, vars, vars_os,
set_var, remove_var, args, args_os, current_exe, temp_dir, home_dir) and
libc::getenv as disallowed methods, and lib.rs forbids allowing them back.
main.rs and the self-spawning integration test carry the only allows.
The forge probe's environment sweep and its 22 guessed variable names go:
the lint covers a variable of any name, so the list was a second, weaker
path for the same rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 20:15:18 +02:00
commit 47d9a35fcd
5 changed files with 41 additions and 62 deletions

View file

@ -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" },
]

View file

@ -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;

View file

@ -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<String> = std::env::args().skip(1).collect();

View file

@ -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;

View file

@ -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::ffi::OsString> = 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",
];