Skip to content

Commit 43ddeee

Browse files
committed
Base plugin folders on namespace, don't allow plugins in subfolders #73
1 parent f1ccc41 commit 43ddeee

File tree

5 files changed

+141
-49
lines changed

5 files changed

+141
-49
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin-examples/random-folder-extender/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ atomic-plugin = { path = "../../atomic-plugin" }
1212
rand = { version = "0.8", features = ["std", "std_rng"] }
1313
serde = { version = "1.0", features = ["derive"] }
1414
serde_json = "1"
15+
toml = "0.9.8"
1516
waki = "0.5.1"

plugin-examples/random-folder-extender/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use atomic_plugin::{ClassExtender, Commit, Resource};
22
use rand::Rng;
3-
use serde::Serialize;
3+
use serde::{Deserialize, Serialize};
44
use waki::Client;
55

66
struct RandomFolderExtender;
@@ -10,10 +10,14 @@ struct DiscordWebhookBody {
1010
content: String,
1111
}
1212

13+
#[derive(Deserialize)]
14+
struct Config {
15+
webhook_url: String,
16+
}
17+
1318
const FOLDER_CLASS: &str = "https://atomicdata.dev/classes/Folder";
1419
const NAME_PROP: &str = "https://atomicdata.dev/properties/name";
1520
const IS_A: &str = "https://atomicdata.dev/properties/isA";
16-
const DISCORD_WEBHOOK_URL: &str = "<YOUR DISCORD WEBHOOK URL>";
1721

1822
fn get_name_from_folder(folder: &Resource) -> Result<&str, String> {
1923
let name = folder
@@ -78,6 +82,9 @@ impl ClassExtender for RandomFolderExtender {
7882
return Ok(());
7983
};
8084

85+
let config_str = std::fs::read_to_string("/config.toml").map_err(|e| e.to_string())?;
86+
let config: Config = toml::from_str(&config_str).map_err(|e| e.to_string())?;
87+
8188
let name = get_name_from_folder(resource)?;
8289
let client = Client::new();
8390

@@ -86,7 +93,7 @@ impl ClassExtender for RandomFolderExtender {
8693
};
8794

8895
let res = client
89-
.post(DISCORD_WEBHOOK_URL)
96+
.post(&config.webhook_url)
9097
.header("Content-Type", "application/json")
9198
.body(serde_json::to_string(&body).map_err(|e| e.to_string())?)
9299
.send()

server/src/appstate.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl AppState {
7373
// Get and register Wasm class extender plugins
7474
let extenders =
7575
wasm::load_wasm_class_extenders(&config.plugin_path, &config.plugin_cache_path, &store)
76-
.await;
76+
.await?;
7777

7878
for extender in extenders {
7979
store.add_class_extender(extender)?;

server/src/plugins/wasm.rs

Lines changed: 128 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::future::Future;
22
use std::pin::Pin;
33

4+
use futures::future::join_all;
5+
46
use std::{
57
collections::HashSet,
68
ffi::OsStr,
@@ -24,7 +26,7 @@ use wasmtime::{
2426
component::{Component, Linker, ResourceTable},
2527
Config, Engine, Store,
2628
};
27-
use wasmtime_wasi::{p2, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
29+
use wasmtime_wasi::{p2, DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
2830
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
2931

3032
mod bindings {
@@ -74,7 +76,7 @@ pub async fn load_wasm_class_extenders(
7476
plugin_path: &Path,
7577
plugin_cache_path: &Path,
7678
db: &Db,
77-
) -> Vec<ClassExtender> {
79+
) -> AtomicResult<Vec<ClassExtender>> {
7880
// Create the plugin directory if it doesn't exist
7981
let plugin_dir = plugin_path.join(CLASS_EXTENDER_DIR_NAME);
8082

@@ -91,7 +93,7 @@ pub async fn load_wasm_class_extenders(
9193
"Created empty Wasm extender directory (drop .wasm files here to enable runtime plugins)"
9294
);
9395
}
94-
return Vec::new();
96+
return Ok(Vec::new());
9597
}
9698

9799
if !plugin_cache_path.exists() {
@@ -108,7 +110,7 @@ pub async fn load_wasm_class_extenders(
108110
Ok(engine) => Arc::new(engine),
109111
Err(err) => {
110112
error!(error = %err, "Failed to initialize Wasm engine. Skipping dynamic class extenders");
111-
return Vec::new();
113+
return Ok(Vec::new());
112114
}
113115
};
114116

@@ -119,43 +121,72 @@ pub async fn load_wasm_class_extenders(
119121

120122
let wasm_files = find_wasm_files(&plugin_dir);
121123

122-
for path in wasm_files {
123-
let wasm_bytes = match std::fs::read(&path) {
124-
Ok(bytes) => bytes,
125-
Err(e) => {
126-
error!("Failed to read Wasm file at {}: {}", path.display(), e);
127-
continue;
128-
}
129-
};
124+
let futures = wasm_files.into_iter().map(|path| {
125+
let plugin_dir = plugin_dir.clone();
126+
let plugin_cache_path = plugin_cache_path.to_path_buf();
127+
let engine = engine.clone();
128+
let db = db.clone();
130129

131-
let hash = digest(&SHA256, &wasm_bytes);
132-
let hash_hex = hex_encode(hash.as_ref());
133-
let cwasm_filename = format!("{}.cwasm", hash_hex);
134-
let cwasm_path = plugin_cache_path.join(cwasm_filename);
130+
async move {
131+
let owned_folder_path = setup_plugin_data_dir(&path, &plugin_dir);
135132

136-
used_cwasm_files.insert(cwasm_path.clone());
137-
138-
match WasmPlugin::load(engine.clone(), &wasm_bytes, &path, &cwasm_path, db).await {
139-
Ok(plugin) => {
140-
info!(
141-
"Loaded {}",
142-
path.file_name().unwrap_or(OsStr::new("Unknown")).display()
143-
);
144-
extenders.push(plugin.into_class_extender());
145-
}
146-
Err(err) => {
147-
error!(
148-
error = %err,
149-
path = %path.display(),
150-
"Failed to load Wasm class extender"
151-
);
133+
let wasm_bytes = match std::fs::read(&path) {
134+
Ok(bytes) => bytes,
135+
Err(e) => {
136+
error!("Failed to read Wasm file at {}: {}", path.display(), e);
137+
return None;
138+
}
139+
};
140+
141+
let hash = digest(&SHA256, &wasm_bytes);
142+
let hash_hex = hex_encode(hash.as_ref());
143+
let cwasm_filename = format!("{}.cwasm", hash_hex);
144+
let cwasm_path = plugin_cache_path.join(cwasm_filename);
145+
146+
let cwasm_path_ret = cwasm_path.clone();
147+
148+
match WasmPlugin::load(
149+
engine.clone(),
150+
&wasm_bytes,
151+
&path,
152+
&cwasm_path,
153+
owned_folder_path,
154+
&db,
155+
)
156+
.await
157+
{
158+
Ok(plugin) => {
159+
info!(
160+
"Loaded {}",
161+
path.file_name().unwrap_or(OsStr::new("Unknown")).display()
162+
);
163+
Some((Some(plugin.into_class_extender()), cwasm_path_ret))
164+
}
165+
Err(err) => {
166+
error!(
167+
error = %err,
168+
path = %path.display(),
169+
"Failed to load Wasm class extender"
170+
);
171+
Some((None, cwasm_path_ret))
172+
}
152173
}
153174
}
175+
});
176+
177+
let results = join_all(futures).await;
178+
179+
for res in results.into_iter().flatten() {
180+
let (extender_opt, cwasm_path) = res;
181+
used_cwasm_files.insert(cwasm_path);
182+
if let Some(extender) = extender_opt {
183+
extenders.push(extender);
184+
}
154185
}
155186

156187
cleanup_cache(&plugin_cache_path, &used_cwasm_files);
157188

158-
extenders
189+
Ok(extenders)
159190
}
160191

161192
fn build_engine() -> AtomicResult<Engine> {
@@ -174,6 +205,7 @@ struct WasmPluginInner {
174205
engine: Arc<Engine>,
175206
component: Component,
176207
path: PathBuf,
208+
owned_folder_path: Option<PathBuf>,
177209
class_url: String,
178210
db: Arc<Db>,
179211
}
@@ -184,6 +216,7 @@ impl WasmPlugin {
184216
wasm_bytes: &[u8],
185217
path: &Path,
186218
cwasm_path: &Path,
219+
owned_folder_path: Option<PathBuf>,
187220
db: &Db,
188221
) -> AtomicResult<Self> {
189222
let db = Arc::new(db.clone());
@@ -218,6 +251,7 @@ impl WasmPlugin {
218251
engine: engine.clone(),
219252
component,
220253
path: path.to_path_buf(),
254+
owned_folder_path,
221255
class_url: String::new(),
222256
db: Arc::clone(&db),
223257
}),
@@ -229,6 +263,7 @@ impl WasmPlugin {
229263
engine,
230264
component: runtime.inner.component.clone(),
231265
path: runtime.inner.path.clone(),
266+
owned_folder_path: runtime.inner.owned_folder_path.clone(),
232267
class_url,
233268
db,
234269
}),
@@ -312,7 +347,7 @@ impl WasmPlugin {
312347
async fn instantiate(&self) -> AtomicResult<(bindings::ClassExtender, Store<PluginHostState>)> {
313348
let mut store = Store::new(
314349
&self.inner.engine,
315-
PluginHostState::new(Arc::clone(&self.inner.db))?,
350+
PluginHostState::new(Arc::clone(&self.inner.db), &self.inner.owned_folder_path)?,
316351
);
317352
let mut linker = Linker::new(&self.inner.engine);
318353
p2::add_to_linker_async(&mut linker).map_err(|err| AtomicError::from(err.to_string()))?;
@@ -403,13 +438,25 @@ struct PluginHostState {
403438
}
404439

405440
impl PluginHostState {
406-
fn new(db: Arc<Db>) -> AtomicResult<Self> {
441+
fn new(db: Arc<Db>, owned_folder_path: &Option<PathBuf>) -> AtomicResult<Self> {
407442
let mut builder = WasiCtxBuilder::new();
408443
builder
409444
.inherit_stdout()
410445
.inherit_stderr()
411446
.inherit_stdin()
412447
.inherit_network();
448+
449+
if let Some(owned_folder_path) = owned_folder_path {
450+
builder
451+
.preopened_dir(
452+
owned_folder_path.clone(),
453+
"/",
454+
DirPerms::READ | DirPerms::MUTATE,
455+
FilePerms::WRITE | FilePerms::READ,
456+
)
457+
.map_err(|e| AtomicError::from(format!("Failed to preopen directory: {}", e)))?;
458+
}
459+
413460
let ctx = builder.build();
414461
Ok(Self {
415462
table: ResourceTable::new(),
@@ -495,23 +542,59 @@ fn find_wasm_files(dir: &Path) -> Vec<PathBuf> {
495542
if let Ok(entries) = std::fs::read_dir(dir) {
496543
for entry in entries.flatten() {
497544
let path = entry.path();
498-
if path.is_dir() {
499-
if let Ok(sub_entries) = std::fs::read_dir(&path) {
500-
for sub_entry in sub_entries.flatten() {
501-
let sub_path = sub_entry.path();
502-
if sub_path.extension() == Some(OsStr::new("wasm")) {
503-
files.push(sub_path);
504-
}
505-
}
506-
}
507-
} else if path.extension() == Some(OsStr::new("wasm")) {
545+
if path.is_file() && path.extension() == Some(OsStr::new("wasm")) {
508546
files.push(path);
509547
}
510548
}
511549
}
512550
files
513551
}
514552

553+
fn setup_plugin_data_dir(wasm_file_path: &Path, plugin_dir: &Path) -> Option<PathBuf> {
554+
let filename = wasm_file_path.file_name().and_then(|s| s.to_str())?;
555+
556+
// Remove .wasm extension
557+
let stem = wasm_file_path
558+
.file_stem()
559+
.and_then(|s| s.to_str())
560+
.unwrap_or(filename);
561+
562+
let stem_path = Path::new(stem);
563+
564+
// If there is no second extension (e.g. just my-plugin.wasm), we don't grant access to a folder.
565+
// This is to prevent plugins from accessing arbitrary folders.
566+
// Only namespaced plugins (e.g. google.calendar.wasm or my-plugin.plugin.wasm) get a folder.
567+
if stem_path.extension().is_none() {
568+
return None;
569+
}
570+
571+
// Remove the second extension (e.g. .plugin in my_script.plugin.wasm), if present.
572+
// This allows for any suffix without dots.
573+
let plugin_name = stem_path
574+
.file_stem()
575+
.and_then(|s| s.to_str())
576+
.unwrap_or(stem);
577+
578+
let data_dir = plugin_dir.join(plugin_name);
579+
580+
if !data_dir.exists() {
581+
if let Err(err) = std::fs::create_dir_all(&data_dir) {
582+
warn!(
583+
error = %err,
584+
path = %data_dir.display(),
585+
"Failed to create data directory for plugin"
586+
);
587+
return None;
588+
}
589+
}
590+
591+
if data_dir.exists() {
592+
Some(data_dir)
593+
} else {
594+
None
595+
}
596+
}
597+
515598
fn compile_and_save_component(
516599
engine: &Engine,
517600
wasm_bytes: &[u8],

0 commit comments

Comments
 (0)