Skip to content

Commit eb9634c

Browse files
Ensure valid wasms when publishing app (#3206)
Signed-off-by: Brian Hardock <brian.hardock@fermyon.com>
1 parent 35fd67b commit eb9634c

File tree

5 files changed

+178
-0
lines changed

5 files changed

+178
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ 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 }
34+
wat = "1"
3335

3436
[dev-dependencies]
3537
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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use anyhow::{bail, Context, Result};
2+
use spin_common::{ui::quoted_path, url::parse_file_url};
3+
use spin_locked_app::locked::{LockedComponent, LockedComponentSource};
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+
let bytes = read_component_source(&component.source).await?;
10+
if !is_wasm_binary(&bytes) {
11+
bail!(
12+
"Component {} source is not a valid .wasm file",
13+
component.id,
14+
);
15+
}
16+
17+
// Ensure that each dependency is a valid wasm binary.
18+
for (dep_name, dep) in &component.dependencies {
19+
let bytes = read_component_source(&dep.source).await?;
20+
if !is_wasm_binary(&bytes) {
21+
bail!(
22+
"dependency {} for component {} is not a valid .wasm file",
23+
dep_name,
24+
component.id,
25+
);
26+
}
27+
}
28+
Ok(())
29+
}
30+
31+
fn is_wasm_binary(bytes: &[u8]) -> bool {
32+
wasmparser::Parser::is_component(bytes)
33+
|| wasmparser::Parser::is_core_wasm(bytes)
34+
|| wat::parse_bytes(bytes).is_ok()
35+
}
36+
37+
async fn read_component_source(source: &LockedComponentSource) -> Result<Vec<u8>> {
38+
let source = source
39+
.content
40+
.source
41+
.as_ref()
42+
.context("LockedComponentSource missing source field")?;
43+
44+
let path = parse_file_url(source)?;
45+
46+
let bytes: Vec<u8> = tokio::fs::read(&path).await.with_context(|| {
47+
format!(
48+
"failed to read component source from disk at path {}",
49+
quoted_path(&path)
50+
)
51+
})?;
52+
Ok(bytes)
53+
}
54+
55+
#[cfg(test)]
56+
mod test {
57+
use super::*;
58+
use crate::from_json;
59+
use spin_locked_app::locked::LockedComponent;
60+
use tokio::io::AsyncWriteExt;
61+
62+
#[tokio::test]
63+
async fn ensures_valid_wasm_binaries() {
64+
let working_dir = tempfile::tempdir().unwrap();
65+
66+
macro_rules! make_locked {
67+
($source:literal, $($dep_name:literal=$dep_path:literal),*) => {
68+
from_json!({
69+
"id": "jiggs",
70+
"source": {
71+
"content_type": "application/wasm",
72+
"source": format!("file://{}", working_dir.path().join($source).to_str().unwrap()),
73+
"digest": "digest",
74+
},
75+
"dependencies": {
76+
$(
77+
$dep_name: {
78+
"source": {
79+
"content_type": "application/wasm",
80+
"source": format!("file://{}", working_dir.path().join($dep_path).to_str().unwrap()),
81+
"digest": "digest",
82+
},
83+
}
84+
),*
85+
}
86+
})
87+
};
88+
}
89+
90+
let make_file = async |name, content| {
91+
let path = working_dir.path().join(name);
92+
93+
let mut file = tokio::fs::File::create(path)
94+
.await
95+
.expect("should create file");
96+
file.write_all(content)
97+
.await
98+
.expect("should write file contents");
99+
};
100+
101+
// valid component source using WAT
102+
make_file("component.wat", b"(component)").await;
103+
// valid module source using WAT
104+
make_file("module.wat", b"(module)").await;
105+
// valid component source
106+
make_file("component.wasm", b"\x00\x61\x73\x6D\x0D\x00\x01\x00").await;
107+
// valid core module source
108+
make_file("module.wasm", b"\x00\x61\x73\x6D\x01\x00\x00\x00").await;
109+
// invalid wasm binary
110+
make_file("invalid.wasm", b"not a wasm file").await;
111+
112+
#[derive(Clone)]
113+
struct TestCase {
114+
name: &'static str,
115+
locked_component: LockedComponent,
116+
valid: bool,
117+
}
118+
119+
let tests: Vec<TestCase> = vec![
120+
TestCase {
121+
name: "Valid Spin component with component WAT",
122+
locked_component: make_locked!("component.wat",),
123+
valid: true,
124+
},
125+
TestCase {
126+
name: "Valid Spin component with module WAT",
127+
locked_component: make_locked!("module.wat",),
128+
valid: true,
129+
},
130+
TestCase {
131+
name: "Valid Spin component with wasm component",
132+
locked_component: make_locked!("component.wasm",),
133+
valid: true,
134+
},
135+
TestCase {
136+
name: "Valid Spin component with wasm core module",
137+
locked_component: make_locked!("module.wasm",),
138+
valid: true,
139+
},
140+
TestCase {
141+
name: "Valid Spin component with wasm dependency",
142+
locked_component: make_locked!("component.wasm", "test:comp2" = "component.wasm"),
143+
valid: true,
144+
},
145+
TestCase {
146+
name: "Invalid Spin component with invalid wasm binary",
147+
locked_component: make_locked!("invalid.wasm",),
148+
valid: false,
149+
},
150+
TestCase {
151+
name: "Valid Spin component with invalid wasm dependency",
152+
locked_component: make_locked!("component.wasm", "test:comp2" = "invalid.wasm"),
153+
valid: false,
154+
},
155+
];
156+
157+
for tc in tests {
158+
let result = ensure_wasms(&tc.locked_component).await;
159+
if tc.valid {
160+
assert!(result.is_ok(), "Test failed: {}", tc.name);
161+
} else {
162+
assert!(result.is_err(), "Test should have failed: {}", tc.name);
163+
}
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)