Skip to content

Commit bbdf38a

Browse files
fix: a package holds only what its importer passes it
1 parent 7e38ae1 commit bbdf38a

21 files changed

Lines changed: 610 additions & 238 deletions

File tree

‎cli/src/asks.rs‎

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use anyhow::{anyhow, bail, Result};
22
use compiler::modules::lock::{locked_spec, verify_pin};
3-
use compiler::modules::rules::HOLDERS;
43
use compiler::modules::{dir_of, join_relative, parse_integrity, walk_up_dirs};
54
use compiler::util::sha256::{hex_encode, sha256};
65
use serde_json::{json, Map, Value};
7-
use std::collections::{HashMap, HashSet};
6+
use std::collections::HashMap;
87
use std::path::{Path, PathBuf};
98
use std::rc::Rc;
109

@@ -27,25 +26,21 @@ impl Home {
2726
}
2827
}
2928

30-
/* A package in the tree, the name the root grants it by and what it asks. */
29+
/* A package the root imports, the key it is granted by and what its section lists. */
3130
pub struct Package {
3231
pub name: String,
3332
label: String,
34-
via: Vec<String>,
3533
pub section: Value,
3634
}
3735

3836
impl Package {
39-
/// The package and the packages it came through, as a report names it.
37+
/// The package as a report names it, its version beside the key it is granted by.
4038
pub fn who(&self) -> String {
41-
match self.via.is_empty() {
42-
true => self.label.clone(),
43-
false => format!("{}, via {}", self.label, self.via.join(" > ")),
44-
}
39+
self.label.clone()
4540
}
4641
}
4742

48-
/* Stops when the root edge.json misses what a package in its tree asks for. */
43+
/* Stops when the root edge.json misses what a package it imports lists. */
4944
pub fn check(path: &Path, lock: &Lock) -> Result<()> {
5045
let Ok(bytes) = std::fs::read(path) else { return Ok(()) };
5146
let manifest: Value = serde_json::from_slice(&bytes).map_err(|e| anyhow!("parsing {}: {e}", path.display()))?;
@@ -56,7 +51,7 @@ pub fn check(path: &Path, lock: &Lock) -> Result<()> {
5651
let imports = manifest.get("imports").and_then(Value::as_object).cloned().unwrap_or_default();
5752
let mut unmet = Vec::new();
5853
for package in packages(path, &imports, lock)?.iter().filter(|p| !p.section.is_null()) {
59-
let missing = system::unmet(&grants, &package.name, &package.section);
54+
let missing = system::unmet(&system::held(&json!([[grants, package.name]])), &package.section);
6055
if !missing.is_empty() {
6156
unmet.push((package.who(), missing.join(", ")));
6257
}
@@ -69,23 +64,30 @@ pub fn check(path: &Path, lock: &Lock) -> Result<()> {
6964
bail!("edge.json does not grant what these packages ask for\n{}\nhelp: grant them under \"permissions\" in edge.json", lines.join("\n"))
7065
}
7166

72-
/* Every package the imports reach, each through its own edge.json and edge.lock. */
67+
/* Every package the root imports, each tree below it held to what its importers pass on. */
7368
pub fn packages(path: &Path, imports: &Map<String, Value>, lock: &Lock) -> Result<Vec<Package>> {
7469
let project = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
7570
// The root grants, so it is never one of the packages it checks.
76-
let mut walk = Walk { seen: HashSet::from([key("", "")]), found: Vec::new() };
77-
walk.imports(&Rc::new(Home::Disk(project.to_path_buf())), "", "", &[], imports, lock)?;
71+
let mut walk = Walk { found: Vec::new(), path: vec![key("", "")] };
72+
walk.imports(&Rc::new(Home::Disk(project.to_path_buf())), "", "", None, imports, lock)?;
7873
Ok(walk.found)
7974
}
8075

76+
/* A non-root importer, as a report names it, and the section it passes on from. */
77+
struct Importer<'a> {
78+
label: &'a str,
79+
section: &'a Value,
80+
}
81+
8182
struct Walk {
82-
seen: HashSet<String>,
8383
found: Vec<Package>,
84+
// The packages from the root down to the one being read.
85+
path: Vec<String>,
8486
}
8587

8688
impl Walk {
8789
/* Follows each import of the package at `dir`, as the lock beside its manifest resolved it. */
88-
fn imports(&mut self, home: &Rc<Home>, id: &str, dir: &str, via: &[String], imports: &Map<String, Value>, lock: &Lock) -> Result<()> {
90+
fn imports(&mut self, home: &Rc<Home>, id: &str, dir: &str, importer: Option<&Importer>, imports: &Map<String, Value>, lock: &Lock) -> Result<()> {
8991
for (name, target) in imports {
9092
// A target the lock cannot answer yet is the command's own error to report.
9193
let Some(spec) = target.as_str().and_then(|t| locked_spec(name, t, Some(lock)).ok()) else { continue };
@@ -101,67 +103,73 @@ impl Walk {
101103
Some(bytes) => bytes,
102104
None => fetch_cached(target, pin).map_err(|e| anyhow!(e))?,
103105
};
104-
self.bundle(target, &spec, &bytes, name, via)?;
106+
self.bundle(target, &spec, &bytes, name, importer)?;
105107
}
106108
continue;
107109
}
108110
let rel = join_relative(dir, target).trim_start_matches("./").to_string();
109111
if packed(&rel) {
110112
let bytes = home.read(&rel).ok_or_else(|| anyhow!("reading {rel}: not found"))?;
111-
self.bundle(&rel, &spec, &bytes, name, via)?;
113+
self.bundle(&rel, &spec, &bytes, name, importer)?;
112114
continue;
113115
}
114-
// A module belongs to the nearest manifest above it, another package unless that is this one.
116+
// A module belongs to the nearest manifest above it, a new package unless it holds the importer.
115117
let nearest = walk_up_dirs(&dir_of(&rel)).find(|d| d == dir || home.read(&format!("{d}edge.json")).is_some());
116-
if let Some(found) = nearest.filter(|d| d != dir) {
118+
if let Some(found) = nearest.filter(|d| !walk_up_dirs(dir).any(|around| around == *d)) {
117119
let place = format!("{found}edge.json");
118-
self.visit(home, id, &found, &place, name, via)?;
120+
self.visit(home, id, &found, &place, name, importer)?;
119121
}
120122
}
121123
Ok(())
122124
}
123125

124126
/* A packed package, held to its pin, then read from inside itself like any other. */
125-
fn bundle(&mut self, address: &str, spec: &str, bytes: &[u8], name: &str, via: &[String]) -> Result<()> {
127+
fn bundle(&mut self, address: &str, spec: &str, bytes: &[u8], name: &str, importer: Option<&Importer>) -> Result<()> {
126128
verify_pin(spec, bytes).map_err(|e| anyhow!(e))?;
127129
let files = crate::pack::into_files(Bundle::decode(bytes).map_err(|e| anyhow!("package '{address}' is not a packed .edge, {e}"))?);
128-
self.visit(&Rc::new(Home::Packed(files)), &hex_encode(&sha256(bytes)), "", address, name, via)
130+
self.visit(&Rc::new(Home::Packed(files)), &hex_encode(&sha256(bytes)), "", address, name, importer)
129131
}
130132

131-
/* Records the package whose manifest sits at `dir`, then follows what it imports in turn. */
132-
fn visit(&mut self, home: &Rc<Home>, id: &str, dir: &str, place: &str, hint: &str, via: &[String]) -> Result<()> {
133-
if !self.seen.insert(key(id, dir)) {
133+
/* Holds the package at `dir` to what its importer passes, then follows its own imports. */
134+
fn visit(&mut self, home: &Rc<Home>, id: &str, dir: &str, place: &str, name: &str, importer: Option<&Importer>) -> Result<()> {
135+
let at = key(id, dir);
136+
// A package importing one that imports it reaches that same package, as the engine reads it.
137+
if self.path.contains(&at) {
134138
return Ok(());
135139
}
136140
let Some(bytes) = home.read(&format!("{dir}edge.json")) else { return Ok(()) };
137141
let manifest: Value = serde_json::from_slice(&bytes).map_err(|e| anyhow!("edge.json at '{place}': {e}"))?;
138-
let named = manifest.get("name").and_then(Value::as_str);
139-
if let Some(name) = named.filter(|n| HOLDERS.contains(n)) {
140-
bail!("edge.json at '{place}': name '{name}' is reserved for permissions");
141-
}
142-
let name = named.unwrap_or(hint).to_string();
143142
let section = manifest.get("permissions").cloned().unwrap_or(Value::Null);
144-
if !section.is_null() {
145-
if let Some(problem) = system::check(&section) {
146-
bail!("edge.json at '{place}': {problem}");
147-
}
148-
// The root grants by name, and against an empty grant every ask comes back unmet.
149-
if named.is_none() && !system::unmet(&json!({}), &name, &section).is_empty() {
150-
bail!("edge.json at '{place}': a package that asks for permissions needs a name");
151-
}
143+
if !section.is_null()
144+
&& let Some(problem) = system::check(&section)
145+
{
146+
bail!("edge.json at '{place}': {problem}");
152147
}
153148
let label = match manifest.get("version").and_then(Value::as_str) {
154149
Some(version) => format!("{name} {version}"),
155-
None => name.clone(),
150+
None => name.to_string(),
156151
};
152+
match importer {
153+
None => self.found.push(Package { name: name.to_string(), label: label.clone(), section: section.clone() }),
154+
Some(importer) if !section.is_null() => {
155+
// What the importer writes for it, since the root check covers what the importer holds.
156+
let missing = system::unmet(&system::held(&json!([[importer.section, name]])), &section);
157+
if !missing.is_empty() {
158+
bail!("edge.json at '{place}' asks for {}, which {} does not pass it", missing.join(", "), importer.label);
159+
}
160+
}
161+
Some(_) => {}
162+
}
157163
let lock = match home.read(&format!("{dir}{}", lock::FILE)) {
158164
Some(bytes) => Lock::parse(&bytes).map_err(|e| anyhow!("edge.json at '{place}': {e}"))?,
159165
None => Lock::default(),
160166
};
161167
let imports = manifest.get("imports").and_then(Value::as_object).cloned().unwrap_or_default();
162-
let chain: Vec<String> = via.iter().cloned().chain([label.clone()]).collect();
163-
self.found.push(Package { name, label, via: via.to_vec(), section });
164-
self.imports(home, id, dir, &chain, &imports, &lock)
168+
let given = if section.is_null() { json!({}) } else { section };
169+
self.path.push(at);
170+
let followed = self.imports(home, id, dir, Some(&Importer { label: &label, section: &given }), &imports, &lock);
171+
self.path.pop();
172+
followed
165173
}
166174
}
167175

‎cli/src/cmd/pkg.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::{anyhow, bail, Result};
22
use compiler::modules::rules::shaped_like_version;
3-
use serde_json::{json, Map, Value};
3+
use serde_json::{Map, Value};
44
use std::io::Read;
55
use std::path::Path;
66

@@ -76,7 +76,7 @@ pub fn add(path: &Path, pkgs: &[String]) -> Result<()> {
7676
let mut asking = false;
7777
for package in packages.iter().filter(|p| !p.section.is_null()) {
7878
// Against an empty grant every ask comes back unmet, which lists them all.
79-
let asked = system::unmet(&json!({}), &package.name, &package.section);
79+
let asked = system::unmet(&[], &package.section);
8080
if !asked.is_empty() {
8181
ui::asks(&package.who(), &asked);
8282
asking = true;

‎cli/src/host/env.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn call_native(caller: &mut Caller<'_, State>, id: i32, call_id: i32, argv_ptr:
9797
}
9898
Ok(status)
9999
}
100-
Native::System { module, name, package } => {
100+
Native::System { module, name, spec } => {
101101
let raw = read(caller, ex.memory, argv_ptr, argc.max(1) * 4);
102102
let handles: Vec<u32> = raw.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c)).collect();
103103
// The trailing slot holds the kwargs, which a system call never takes.
@@ -117,7 +117,7 @@ fn call_native(caller: &mut Caller<'_, State>, id: i32, call_id: i32, argv_ptr:
117117
}
118118
let state = caller.data();
119119
let called = match state.events.clone() {
120-
Some(events) => super::system::invoke(state.run, &package, &module, &name, &args, call_id as u32, events),
120+
Some(events) => super::system::invoke(state.run, &spec, &name, &args, call_id as u32, events),
121121
None => Err(format!("RuntimeError: {module}.{name} has no interpreter to answer")),
122122
};
123123
let answer = called.map(|called| match called {

‎cli/src/host/mod.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ pub struct Plugin {
192192
#[derive(Clone)]
193193
pub enum Native {
194194
Plugin(Box<Plugin>),
195-
// A system call, opened for one package and answered by SpiderMonkey.
195+
// A system call, opened under the spec its package imports it by, answered by SpiderMonkey.
196196
System {
197197
module: String,
198198
name: String,
199-
package: String,
199+
spec: String,
200200
},
201201
}
202202

‎cli/src/host/resolver.rs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,15 @@ fn registered(names: &[String]) -> Vec<String> {
125125
})
126126
}
127127

128-
/* Serves the system modules to every package the walk met, opened with its scopes or refused when the root grants it none. */
128+
/* Serves the system modules to every package the walk met, refused where its importers pass none. */
129129
fn serve_system(inst: &mut Instance, project: &Project, packages: &Value) -> Vec<String> {
130130
let root = packages["root"].as_str().unwrap_or_default();
131131
// An untrusted run grants only through its own bundle manifest, and an empty grant needs no checking.
132132
let declared = packages.get("permissions").filter(|p| (!project.untrusted || project.ceiling.is_some()) && p.as_object().is_some_and(|holders| !holders.is_empty())).cloned();
133133
if let Some(problem) = declared.as_ref().and_then(system::check) {
134134
return vec![format!("edge.json at '{root}edge.json': {problem}")];
135135
}
136+
let granted = declared.is_some();
136137
let permissions = declared.unwrap_or_else(|| json!({}));
137138
let entries: Vec<&Value> = permissions.as_object().into_iter().flat_map(|holders| holders.values()).filter_map(Value::as_array).flatten().collect();
138139
// The refusal names only what the bundle grants, never the eval grant of the pool.
@@ -144,7 +145,7 @@ fn serve_system(inst: &mut Instance, project: &Project, packages: &Value) -> Vec
144145
.into_iter()
145146
.flatten()
146147
.filter_map(|(holder, listed)| {
147-
let missing = system::unmet(&json!({ "eval": ceiling }), "eval", &json!({ "main": listed }));
148+
let missing = system::unmet(&system::held(&json!([[{ "eval": ceiling }, "eval"]])), &json!({ "main": listed }));
148149
(!missing.is_empty()).then(|| (holder, missing.join(", ")))
149150
})
150151
.collect();
@@ -180,9 +181,11 @@ fn serve_system(inst: &mut Instance, project: &Project, packages: &Value) -> Vec
180181
}
181182
for pair in packages["dirs"].as_array().into_iter().flatten() {
182183
let (Some(dir), Some(pkg)) = (pair[0].as_str(), pair[1].as_str()) else { continue };
184+
// A run that grants nothing of its own holds nothing, whatever its manifests pass on.
185+
let chain = if granted { pair[2].clone() } else { json!([]) };
183186
for module in SYSTEM_MODULES {
184187
let spec = system_spec(module, dir);
185-
let served = match system::scopes(&permissions, pkg, module) {
188+
let served = match system::scopes(&chain, module) {
186189
Some(held) => inst.register_system(&spec, pkg, module, &held),
187190
None => inst.register_error(&spec, &format!("'{pkg}' imports {module}, which edge.json does not grant it")),
188191
};

‎cli/src/host/system/engine.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::thread::JoinHandle;
2121
const WEB: &str = include_str!("web.js");
2222
// The only JavaScript that can ever load, the system calls `js/dist` carries.
2323
const SYSTEM_DIR: &str = "src/system/";
24-
const ENTRY: &str = "import { SYSTEM } from './system/index.js';\nimport { check, scopes, unmet } from './system/grants.js';\nglobalThis.__edge = { open: SYSTEM, check, scopes, unmet };\n";
24+
const ENTRY: &str = "import { SYSTEM } from './system/index.js';\nimport { check, held, scopes, unmet } from './system/grants.js';\nglobalThis.__edge = { open: SYSTEM, check, held, scopes, unmet };\n";
2525

2626
/* What the thread keeps between calls, the calls waiting to settle and the pipe's requests and sockets. */
2727
struct State {

0 commit comments

Comments
 (0)