Skip to content

Commit 7f0c1ce

Browse files
committed
fix(cli): tighten Wasm tool validation
Signed-off-by: akrm al-hakimi <alhakimiakrmj@gmail.com>
1 parent b7c661c commit 7f0c1ce

1 file changed

Lines changed: 73 additions & 10 deletions

File tree

host/src/wasm_host_fns.rs

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,17 @@ impl WasmToolOptions {
6161
}
6262

6363
let mut merged_env = Vec::with_capacity(env.len() + inherit_env.len());
64-
for spec in env {
65-
merged_env.push(parse_env_pair(spec)?);
66-
}
6764
for key in inherit_env {
6865
if key.is_empty() {
6966
bail!("--tool-wasi-env-inherit key must not be empty");
7067
}
7168
let value = std::env::var(key)
7269
.with_context(|| format!("inherit environment variable {key}"))?;
73-
merged_env.push((key.clone(), value));
70+
set_env_pair(&mut merged_env, key.clone(), value);
71+
}
72+
for spec in env {
73+
let (key, value) = parse_env_pair(spec)?;
74+
set_env_pair(&mut merged_env, key, value);
7475
}
7576

7677
Ok(Self {
@@ -134,7 +135,7 @@ impl WasmTool {
134135
}
135136

136137
pub fn invoke(&self, args: Value) -> Result<Value> {
137-
let request = serde_json::json!({ "name": self.name, "args": args });
138+
let request = serde_json::json!({ "name": &self.name, "args": args });
138139
let stdin = serde_json::to_vec(&request)?;
139140
let stdout = MemoryOutputPipe::new(self.options.output_limit);
140141
let stderr = MemoryOutputPipe::new(self.options.output_limit);
@@ -214,7 +215,7 @@ impl WasmTool {
214215
Ok(value) => Ok(value),
215216
Err(err) if stdout_len >= self.options.output_limit => Err(err).with_context(|| {
216217
format!(
217-
"Wasm tool {} stdout reached output limit of {} bytes",
218+
"Wasm tool {} stdout may have reached output limit of {} bytes",
218219
self.name, self.options.output_limit
219220
)
220221
}),
@@ -228,6 +229,7 @@ fn parse_tool_spec(spec: &str) -> Result<(String, PathBuf)> {
228229
.split_once('=')
229230
.ok_or_else(|| anyhow!("--tool must use NAME=WASM syntax: {spec}"))?;
230231
let name = name.trim();
232+
let path = path.trim();
231233
if name.is_empty() {
232234
bail!("--tool name must not be empty: {spec}");
233235
}
@@ -244,10 +246,15 @@ fn parse_wasi_dir(spec: &str, read_only: bool) -> Result<WasiDir> {
244246
let (host, guest) = if let Some(idx) = spec.rfind(':') {
245247
let (host, guest) = spec.split_at(idx);
246248
let guest = &guest[1..];
247-
if guest.starts_with('/') || guest == "." || guest.starts_with("./") {
249+
if is_windows_drive_path(spec, idx) {
250+
(spec, "/host")
251+
} else if guest.starts_with('/') || guest == "." || guest.starts_with("./") {
248252
(host, guest)
249253
} else {
250-
(spec, "/host")
254+
bail!(
255+
"invalid WASI preopen guest path {:?}: expected absolute path, '.', or './path'",
256+
guest
257+
);
251258
}
252259
} else {
253260
(spec, "/host")
@@ -279,6 +286,31 @@ fn parse_env_pair(spec: &str) -> Result<(String, String)> {
279286
Ok((key.to_string(), value.to_string()))
280287
}
281288

289+
fn set_env_pair(env: &mut Vec<(String, String)>, key: String, value: String) {
290+
if let Some((_, existing)) = env
291+
.iter_mut()
292+
.find(|(existing_key, _)| existing_key == &key)
293+
{
294+
*existing = value;
295+
} else {
296+
env.push((key, value));
297+
}
298+
}
299+
300+
fn is_windows_drive_path(spec: &str, colon_idx: usize) -> bool {
301+
colon_idx == 1
302+
&& spec
303+
.as_bytes()
304+
.first()
305+
.map(|b| b.is_ascii_alphabetic())
306+
.unwrap_or(false)
307+
&& spec
308+
.as_bytes()
309+
.get(2)
310+
.map(|b| *b == b'/' || *b == b'\\')
311+
.unwrap_or(false)
312+
}
313+
282314
fn pipe_text(pipe: &MemoryOutputPipe) -> String {
283315
String::from_utf8_lossy(&pipe.contents()).into_owned()
284316
}
@@ -790,7 +822,7 @@ mod tests {
790822

791823
#[test]
792824
fn parse_tool_spec_accepts_valid_name_and_path() {
793-
let (name, path) = parse_tool_spec("greet=./handler.wasm").unwrap();
825+
let (name, path) = parse_tool_spec(" greet = ./handler.wasm ").unwrap();
794826
assert_eq!(name, "greet");
795827
assert_eq!(path, PathBuf::from("./handler.wasm"));
796828
}
@@ -805,6 +837,7 @@ mod tests {
805837
"fs_read=handler.wasm",
806838
"net_socket=handler.wasm",
807839
"greet=",
840+
"greet= ",
808841
] {
809842
assert!(parse_tool_spec(spec).is_err(), "{spec} should fail");
810843
}
@@ -847,6 +880,27 @@ mod tests {
847880
assert_eq!(opts.dirs[0].guest, "/host");
848881
}
849882

883+
#[test]
884+
fn cli_options_use_last_explicit_env_value_for_duplicate_keys() {
885+
let opts = WasmToolOptions::from_cli(
886+
&[],
887+
&[],
888+
&[
889+
"A=first".to_string(),
890+
"B=only".to_string(),
891+
"A=second".to_string(),
892+
],
893+
&[],
894+
1,
895+
1,
896+
)
897+
.unwrap();
898+
assert_eq!(
899+
opts.env,
900+
vec![("A".into(), "second".into()), ("B".into(), "only".into())]
901+
);
902+
}
903+
850904
#[test]
851905
fn cli_options_reject_invalid_values() {
852906
let dir = tempdir("invalid-options");
@@ -857,6 +911,15 @@ mod tests {
857911
);
858912
assert!(WasmToolOptions::from_cli(&[], &[], &["=value".to_string()], &[], 1, 1).is_err());
859913
assert!(WasmToolOptions::from_cli(&[], &[], &[], &["".to_string()], 1, 1).is_err());
914+
assert!(WasmToolOptions::from_cli(
915+
&[format!("{}:relative", dir.display())],
916+
&[],
917+
&[],
918+
&[],
919+
1,
920+
1,
921+
)
922+
.is_err());
860923
assert!(WasmToolOptions::from_cli(
861924
&[
862925
format!("{}:/dup", dir.display()),
@@ -1051,7 +1114,7 @@ mod tests {
10511114
);
10521115
let err = tool.invoke(json!({})).unwrap_err();
10531116
let msg = err_string(err);
1054-
assert!(msg.contains("stdout reached output limit of 8 bytes"));
1117+
assert!(msg.contains("stdout may have reached output limit of 8 bytes"));
10551118
assert!(msg.contains("wrote non-JSON stdout"));
10561119
}
10571120

0 commit comments

Comments
 (0)