oxedyne/daimond/hand/examples/buildfence.rs
7.5 KiB, 1 run
created by r2519314175:897, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // buildfence -- can a fenced `cargo` compile this repository? |
| 2 | // |
| 3 | // Written on 2026-08-25 to settle BLOCKERS B6 with a measurement rather than an |
| 4 | // argument. The question is whether a daimon can be given a truthful "does this |
| 5 | // build" through the ordinary `run` door, inside the fence that door applies. |
| 6 | // Reasoning about it is unreliable in the same way `fenceprobe` is kept for: |
| 7 | // Landlock's union semantics do not read the way they behave, and cargo's own |
| 8 | // failure for an unreadable path dependency is a manifest error rather than a |
| 9 | // permission one, so a reader who has not seen it guesses wrong. |
| 10 | // |
| 11 | // cargo run --release --example buildfence --manifest-path hand/Cargo.toml -- <root> |
| 12 | // cargo run --release --example buildfence --manifest-path hand/Cargo.toml -- <root> --with-fe2o3 |
| 13 | // cargo run --release --example buildfence --manifest-path hand/Cargo.toml -- <root> --with-fe2o3 --deny-history |
| 14 | // |
| 15 | // WHAT IT ANSWERED, 2026-08-25, measured on this machine and recorded here so the |
| 16 | // next reader does not have to spend the build: without fe2o3 the fenced cargo |
| 17 | // cannot read `fe2o3_core/Cargo.toml` and stops there with Permission denied; |
| 18 | // with fe2o3 read-only a COLD `cargo check` of the hand compiles four fe2o3 |
| 19 | // crates from source in 12 s; and denying that tree's `.ore` and `.git` inside |
| 20 | // the same grant costs the build nothing while both histories stay refused. |
| 21 | // |
| 22 | // `<root>` is the folder the user granted, and defaults to the working |
| 23 | // directory. The |
| 24 | // second form adds the fe2o3 tree read-only, which is the widening the blocker is |
| 25 | // asking about; the third denies that tree's `.ore` and `.git` inside the same |
| 26 | // grant, which is the question of whether the widening can be made narrow enough |
| 27 | // to be worth taking. The three runs together say what each costs. |
| 28 | use daimond_hand::fence::{Fence, Unfenced}; |
| 29 | use daimond_hand::wire::FenceSpec; |
| 30 | |
| 31 | use std::path::PathBuf; |
| 32 | use std::process::Command; |
| 33 | |
| 34 | // Where fe2o3 lives, which every crate in this repository depends on BY PATH. |
| 35 | // Overridable, because the answer is about the SHAPE of the dependency and not |
| 36 | // about this machine: set BUILDFENCE_FE2O3 to ask the same question elsewhere. |
| 37 | const FE2O3_DEFAULT: &str = "/home/jason/usr/code/rust/fe2o3"; |
| 38 | |
| 39 | fn main() { |
| 40 | let args: Vec<String> = std::env::args().skip(1).collect(); |
| 41 | let with_fe2o3 = args.iter().any(|a| a == "--with-fe2o3"); |
| 42 | let deny_history = args.iter().any(|a| a == "--deny-history"); |
| 43 | // A reflux fixture is a whole crate at the granted root, so the question there |
| 44 | // is asked of the root itself rather than of `hand/`. |
| 45 | let lib_here = args.iter().any(|a| a == "--lib-here"); |
| 46 | let root = match args.iter().find(|a| !a.starts_with("--")) { |
| 47 | Some(r) => r.clone(), |
| 48 | None => match std::env::current_dir() { |
| 49 | Ok(d) => format!("{}", d.display()), |
| 50 | Err(e) => { println!("no root given and no working directory: {}", e); return; }, |
| 51 | }, |
| 52 | }; |
| 53 | let fe2o3 = std::env::var("BUILDFENCE_FE2O3") |
| 54 | .unwrap_or_else(|_| FE2O3_DEFAULT.to_string()); |
| 55 | let home = match std::env::var("HOME") { |
| 56 | Ok(h) => h, |
| 57 | Err(_) => { println!("no HOME, so no toolkit roots to compute."); return; }, |
| 58 | }; |
| 59 | // The rust toolkit's roots, exactly as `Toolkit::grants` names them in the |
| 60 | // app and `TOOLKIT_ROOTS` clamps them in the hand. |
| 61 | let mut rw: Vec<String> = vec![ |
| 62 | root.clone(), |
| 63 | format!("{}/.cargo/registry", home), |
| 64 | format!("{}/.cargo/git", home), |
| 65 | format!("{}/.cargo/.package-cache", home), |
| 66 | format!("{}/.cache/cargo-targets", home), |
| 67 | ]; |
| 68 | let mut ro: Vec<String> = vec![ |
| 69 | format!("{}/.cargo/bin", home), |
| 70 | format!("{}/.rustup", home), |
| 71 | ]; |
| 72 | let mut deny: Vec<String> = Vec::new(); |
| 73 | if with_fe2o3 { |
| 74 | ro.push(fe2o3.clone()); |
| 75 | } |
| 76 | if deny_history { |
| 77 | deny.push(format!("{}/.ore", fe2o3)); |
| 78 | deny.push(format!("{}/.git", fe2o3)); |
| 79 | } |
| 80 | // A scratch, because `Scratch` gives every real run one and points TMPDIR at |
| 81 | // it -- without it a fenced cargo dies part way through on a temp directory. |
| 82 | let scratch = format!("{}/.cache/daimond/buildfence-scratch", home); |
| 83 | if let Err(e) = std::fs::create_dir_all(&scratch) { |
| 84 | println!("could not make a scratch at {}: {}", scratch, e); |
| 85 | return; |
| 86 | } |
| 87 | rw.push(scratch.clone()); |
| 88 | |
| 89 | let spec = FenceSpec { rw: rw.clone(), ro: ro.clone(), deny: deny.clone(), net: false }; |
| 90 | println!("root {}", root); |
| 91 | println!("rw {}", rw.join("\n ")); |
| 92 | println!("ro {}", ro.join("\n ")); |
| 93 | println!("deny {}", if deny.is_empty() { "(none)".to_string() } else { deny.join("\n ") }); |
| 94 | println!("net false"); |
| 95 | |
| 96 | let f = Fence::detect(); |
| 97 | let plan = match f.plan(&spec, &Unfenced::Refuse) { |
| 98 | Ok(p) => p, |
| 99 | Err(e) => { println!("plan failed: {}", e); return; }, |
| 100 | }; |
| 101 | match plan.apply() { |
| 102 | Ok(_) => println!("--- fence applied ---"), |
| 103 | Err(e) => { println!("apply failed: {}", e); return; }, |
| 104 | } |
| 105 | |
| 106 | // What the widening exposes, asked directly rather than inferred: a source |
| 107 | // file of fe2o3, and then the two histories. The signed one holds a key. |
| 108 | for probe in [ |
| 109 | format!("{}/fe2o3_core/Cargo.toml", fe2o3), |
| 110 | format!("{}/.ore/config", fe2o3), |
| 111 | format!("{}/.git/HEAD", fe2o3), |
| 112 | ] { |
| 113 | println!("read {:<58} {}", probe, |
| 114 | match std::fs::File::open(&probe) { Ok(_) => "PERMITTED", Err(_) => "refused" }); |
| 115 | } |
| 116 | |
| 117 | // The question, asked of the cheapest thing that has to read every manifest: |
| 118 | // `cargo metadata` resolves the graph and nothing else, so a refusal here is |
| 119 | // about READING the dependency and not about compiling it. |
| 120 | let jobs = match lib_here { |
| 121 | true => vec![("check --lib, at the root", vec!["check", "--offline", "--lib", |
| 122 | "--message-format", "short"])], |
| 123 | false => vec![ |
| 124 | ("metadata, repository root", vec!["metadata", "--offline", "--no-deps", |
| 125 | "--format-version", "1", "--manifest-path", "Cargo.toml"]), |
| 126 | ("check, the hand", vec!["check", "--offline", "--message-format", "short", |
| 127 | "--manifest-path", "hand/Cargo.toml"]), |
| 128 | ], |
| 129 | }; |
| 130 | for (what, argv) in jobs { |
| 131 | let out = Command::new("cargo") |
| 132 | .args(&argv) |
| 133 | .current_dir(PathBuf::from(&root)) |
| 134 | .env_clear() |
| 135 | .env("HOME", &home) |
| 136 | .env("PATH", format!("{}/.cargo/bin:/usr/local/bin:/usr/bin:/bin", home)) |
| 137 | .env("TMPDIR", &scratch) |
| 138 | // Inherited, so the probe measures the same build a caller would get. |
| 139 | // Named explicitly rather than passed through by `env_clear`'s absence, |
| 140 | // because a fenced cargo writing somewhere the fence does not grant is |
| 141 | // the second way this question is answered wrongly. |
| 142 | .env("CARGO_TARGET_DIR", std::env::var("CARGO_TARGET_DIR") |
| 143 | .unwrap_or_else(|_| format!("{}/.cache/cargo-targets/buildfence", home))) |
| 144 | .output(); |
| 145 | match out { |
| 146 | Ok(o) => { |
| 147 | let err = String::from_utf8_lossy(&o.stderr); |
| 148 | println!("\n== {} -> exit {:?}", what, o.status.code()); |
| 149 | for line in err.lines().take(8) { |
| 150 | println!(" {}", line); |
| 151 | } |
| 152 | }, |
| 153 | Err(e) => println!("\n== {} -> could not spawn cargo: {}", what, e), |
| 154 | } |
| 155 | } |
| 156 | } |