Run world service policy in WASI init with pollable process observation

This commit is contained in:
2026-11-06 19:49:19 +00:00
parent b5462cc366
commit b39bbd4f18
21 changed files with 1821 additions and 224 deletions
+4
View File
@@ -67,6 +67,10 @@ pub trait File: Send + Sync {
/// Implementations must retain open file identity and return portable IO errors.
#[async_trait]
pub trait Layer: Send + Sync {
/// Shared observation of mutations in this namespace, when available.
fn observer(&self) -> Option<observe::Observer> {
None
}
async fn stat(&self, path: &str, follow: bool) -> Result<Stat>;
/// Return the target of a symbolic link, or InvalidInput for another node.
async fn readlink(&self, _path: &str) -> Result<String> {
+15 -3
View File
@@ -6,15 +6,24 @@ struct Mount {
readonly: bool,
}
/// A per-process mount namespace. Mount the root before use.
#[derive(Default)]
pub struct Mounts {
mounts: BTreeMap<String, Mount>,
observer: observe::Observer,
}
impl Default for Mounts {
fn default() -> Self {
Self {
mounts: BTreeMap::new(),
observer: observe::Observer::new(|_| {}),
}
}
}
impl Mounts {
/// Add or replace a mount. Read-only mounts reject every mutation.
pub fn mount(&mut self, path: &str, layer: Arc<dyn Layer>, readonly: bool) {
self.mounts
.insert(resolve("/", path), Mount { layer, readonly });
let path = resolve("/", path);
let layer = self.observer.layer(layer, &path, Process::default());
self.mounts.insert(path, Mount { layer, readonly });
}
pub(crate) async fn metadata_file(&self, path: &str, follow: bool) -> Result<Arc<dyn File>> {
let (m, p) = self.route(path, true)?;
@@ -49,6 +58,9 @@ impl Mounts {
}
#[async_trait]
impl Layer for Mounts {
fn observer(&self) -> Option<observe::Observer> {
Some(self.observer.clone())
}
async fn stat(&self, path: &str, follow: bool) -> Result<Stat> {
let (m, p) = self.route(path, false)?;
m.layer.stat(&p, follow).await
+43 -8
View File
@@ -6,6 +6,14 @@ use std::{
};
use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
/// The successful namespace or content mutation observed at a world path.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChangeKind {
Created,
Modified,
Removed,
}
type Namespace = Mutex<BTreeMap<String, Weak<RwLock<()>>>>;
enum Held {
Read { _guard: OwnedRwLockReadGuard<()> },
@@ -20,6 +28,7 @@ type Callback = dyn Fn(&str) + Send + Sync;
#[derive(Clone)]
pub struct Observer {
changed: Arc<Callback>,
events: tokio::sync::broadcast::Sender<(String, ChangeKind)>,
paths: Arc<Mutex<Vec<Weak<TrackedPath>>>>,
// Only open-registration versus rename/remove needs ordering. Lock the affected
// subtrees, not all IO: a blocked open must not stall unrelated namespaces.
@@ -29,10 +38,19 @@ impl Observer {
pub fn new(changed: impl Fn(&str) + Send + Sync + 'static) -> Self {
Self {
changed: Arc::new(changed),
events: tokio::sync::broadcast::channel(256).0,
paths: Arc::default(),
namespace: Arc::default(),
}
}
/// Subscribe before the first mutation of interest. Lag means callers must rescan.
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<(String, ChangeKind)> {
self.events.subscribe()
}
fn notify(&self, path: &str, kind: ChangeKind) {
(self.changed)(path);
let _ = self.events.send((path.to_owned(), kind));
}
/// Wrap a layer at its canonical namespace prefix (e.g. `/run`).
pub fn layer(&self, layer: Arc<dyn Layer>, prefix: &str, process: Process) -> Arc<dyn Layer> {
Arc::new(Observed {
@@ -149,7 +167,7 @@ impl Handle {
let result = f.await?;
let path = path.lock().unwrap().clone();
if let Some(path) = path {
(observer.changed)(&path);
observer.notify(&path, ChangeKind::Modified);
}
Ok(result)
})
@@ -178,6 +196,7 @@ impl Layer for Observed {
self.process
.operation(async move {
let order = observer.order(&[(&path, false)]).await;
let existed = !options.create || layer.stat(&local, options.follow).await.is_ok();
let file = layer.open(&local, options).await?;
let handle = Arc::new(Handle {
file,
@@ -187,7 +206,14 @@ impl Layer for Observed {
});
drop(order);
if options.create || options.truncate {
(observer.changed)(&path);
observer.notify(
&path,
if existed {
ChangeKind::Modified
} else {
ChangeKind::Created
},
);
}
Ok(handle as Arc<dyn File>)
})
@@ -203,7 +229,7 @@ impl Layer for Observed {
self.process
.operation(async move {
layer.mkdir(&local).await?;
(observer.changed)(&path);
observer.notify(&path, ChangeKind::Created);
Ok(())
})
.await
@@ -226,13 +252,22 @@ impl Layer for Observed {
self.process
.operation(async move {
let order = observer.order(&[(&from, true), (&to, true)]).await;
let replaced = layer.stat(&b, false).await.is_ok();
layer.rename(&a, &b).await?;
if from != to {
observer.moved(&from, Some(&to));
if from == to {
return Ok(());
}
observer.moved(&from, Some(&to));
drop(order);
(observer.changed)(&from);
(observer.changed)(&to);
observer.notify(&from, ChangeKind::Removed);
observer.notify(
&to,
if replaced {
ChangeKind::Modified
} else {
ChangeKind::Created
},
);
Ok(())
})
.await
@@ -256,7 +291,7 @@ impl Observed {
}
observer.moved(&path, None);
drop(order);
(observer.changed)(&path);
observer.notify(&path, ChangeKind::Removed);
Ok(())
})
.await