Skip to content

Commit e4c7bee

Browse files
committed
Ensure valid wasms when publishing app
Signed-off-by: Brian Hardock <[email protected]>
1 parent 35fd67b commit e4c7bee

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
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.

crates/oci/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ tokio = { workspace = true, features = ["fs"] }
3030
tokio-util = { version = "0.7", features = ["compat"] }
3131
tracing = { workspace = true }
3232
walkdir = { workspace = true }
33+
wasmparser = { workspace = true }
3334

3435
[dev-dependencies]
3536
wasm-encoder = { workspace = true }

crates/oci/src/client.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use tokio::fs;
2424
use walkdir::WalkDir;
2525

2626
use crate::auth::AuthConfig;
27+
use crate::validate;
2728

2829
// TODO: the media types for application, data and archive layer are not final
2930
/// Media type for a layer representing a locked Spin application configuration
@@ -149,6 +150,12 @@ impl Client {
149150
)
150151
.await?;
151152

153+
// Ensure that all Spin components specify valid wasm binaries in both the `source`
154+
// field and for each dependency.
155+
for locked_component in &locked.components {
156+
validate::ensure_wasms(locked_component).await?;
157+
}
158+
152159
self.push_locked_core(
153160
locked,
154161
auth,

crates/oci/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod auth;
55
pub mod client;
66
mod loader;
77
pub mod utils;
8+
mod validate;
89

910
pub use client::{Client, ComposeMode};
1011
pub use loader::OciLoader;

crates/oci/src/validate.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use anyhow::{bail, Context, Result};
2+
use spin_locked_app::locked::LockedComponent;
3+
use tokio::fs;
4+
5+
// Validate that all Spin components specify valid wasm binaries in both the `source`
6+
// field and for each dependency.
7+
pub async fn ensure_wasms(component: &LockedComponent) -> Result<()> {
8+
// Ensure that the component source is a valid wasm binary.
9+
if let Some(source) = &component.source.content.source {
10+
let bytes = fs::read(&source)
11+
.await
12+
.with_context(|| format!("Failed to read component `{}` source", component.id))?;
13+
14+
if !is_wasm_binary(&bytes) {
15+
bail!(
16+
"Component {} source is not a valid .wasm file",
17+
component.id,
18+
);
19+
}
20+
}
21+
// Ensure that each dependency is a valid wasm binary.
22+
for (dep_name, dep) in &component.dependencies {
23+
if let Some(source) = &dep.source.content.source {
24+
let bytes = fs::read(&source).await.with_context(|| {
25+
format!(
26+
"Failed to read dependency `{}` for component `{}`",
27+
dep_name, component.id
28+
)
29+
})?;
30+
if !is_wasm_binary(&bytes) {
31+
bail!(
32+
"Dependency {} for component {} is not a valid .wasm file",
33+
dep_name,
34+
component.id,
35+
);
36+
}
37+
}
38+
}
39+
Ok(())
40+
}
41+
42+
fn is_wasm_binary(bytes: &[u8]) -> bool {
43+
wasmparser::Parser::is_component(bytes) || wasmparser::Parser::is_core_wasm(bytes)
44+
}
45+
46+
#[cfg(test)]
47+
mod test {
48+
use super::*;
49+
use crate::from_json;
50+
use spin_locked_app::locked::LockedComponent;
51+
use tokio::io::AsyncWriteExt;
52+
53+
#[tokio::test]
54+
async fn ensures_valid_wasm_binaries() {
55+
let working_dir = tempfile::tempdir().unwrap();
56+
let _ = tokio::fs::create_dir(working_dir.path()).await;
57+
58+
// valid component source
59+
let mut valid_wasm_component_file =
60+
tokio::fs::File::create(working_dir.path().join("component.wasm"))
61+
.await
62+
.expect("should create component file");
63+
valid_wasm_component_file
64+
.write_all(b"\x00\x61\x73\x6D\x0D\x00\x01\x00")
65+
.await
66+
.expect("should write file contents");
67+
68+
// valid core module source
69+
let mut valid_wasm_core_module_file =
70+
tokio::fs::File::create(working_dir.path().join("module.wasm"))
71+
.await
72+
.expect("should create core module file");
73+
valid_wasm_core_module_file
74+
.write_all(b"\x00\x61\x73\x6D\x01\x00\x00\x00")
75+
.await
76+
.expect("should write file contents");
77+
78+
// invalid wasm binary
79+
let mut invalid_wasm_binary = tokio::fs::File::create(working_dir.path().join("invalid"))
80+
.await
81+
.expect("should create file");
82+
invalid_wasm_binary
83+
.write_all(b"not a wasm file")
84+
.await
85+
.expect("should write file contents");
86+
87+
#[derive(Clone)]
88+
struct TestCase {
89+
name: &'static str,
90+
locked_component: LockedComponent,
91+
valid: bool,
92+
}
93+
94+
let tests: Vec<TestCase> = vec![
95+
TestCase {
96+
name: "Valid Spin component source field with component",
97+
locked_component: from_json!({
98+
"id": "component",
99+
"source": {
100+
"content_type": "application/wasm",
101+
"source": working_dir.path().join("component.wasm").to_str().unwrap().to_string(),
102+
"digest": "digest",
103+
}
104+
}),
105+
valid: true,
106+
},
107+
TestCase {
108+
name: "Valid Spin component source field with core module",
109+
locked_component: from_json!({
110+
"id": "component",
111+
"source": {
112+
"content_type": "application/wasm",
113+
"source": working_dir.path().join("module.wasm").to_str().unwrap().to_string(),
114+
"digest": "digest",
115+
}
116+
}),
117+
valid: true,
118+
},
119+
TestCase {
120+
name: "Valid Spin component source field with valid wasm dependency",
121+
locked_component: from_json!({
122+
"id": "component",
123+
"source": {
124+
"content_type": "application/wasm",
125+
"source": working_dir.path().join("component.wasm").to_str().unwrap().to_string(),
126+
"digest": "digest",
127+
},
128+
"dependencies": {
129+
"test:comp2": {
130+
"source": {
131+
"content_type": "application/wasm",
132+
"source": working_dir.path().join("component.wasm").to_str().unwrap().to_string(),
133+
"digest": "digest",
134+
},
135+
"export": null,
136+
}
137+
}
138+
}),
139+
valid: true,
140+
},
141+
TestCase {
142+
name: "Invalid Spin component source field with invalid wasm binary",
143+
locked_component: from_json!({
144+
"id": "component",
145+
"source": {
146+
"content_type": "application/wasm",
147+
"source": working_dir.path().join("invalid").to_str().unwrap().to_string(),
148+
"digest": "digest",
149+
}
150+
}),
151+
valid: false,
152+
},
153+
TestCase {
154+
name: "Valid Spin component source field with invalid wasm dependency",
155+
locked_component: from_json!({
156+
"id": "component",
157+
"source": {
158+
"content_type": "application/wasm",
159+
"source": working_dir.path().join("component.wasm").to_str().unwrap().to_string(),
160+
"digest": "digest",
161+
},
162+
"dependencies": {
163+
"test:comp2": {
164+
"source": {
165+
"content_type": "application/wasm",
166+
"source": working_dir.path().join("invalid").to_str().unwrap().to_string(),
167+
"digest": "digest",
168+
},
169+
"export": null,
170+
}
171+
}
172+
}),
173+
valid: false,
174+
},
175+
];
176+
177+
for tc in tests {
178+
let result = ensure_wasms(&tc.locked_component).await;
179+
if tc.valid {
180+
assert!(result.is_ok(), "Test failed: {}", tc.name);
181+
} else {
182+
assert!(result.is_err(), "Test should have failed: {}", tc.name);
183+
}
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)