1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
use super::*;
use crate::ToWasmtimeResult as _;
impl<'a> CodeBuilder<'a> {
pub(crate) fn get_compile_time_builtins(&self) -> &HashMap<Cow<'a, str>, Cow<'a, [u8]>> {
&self.compile_time_builtins
}
pub(super) fn compose_compile_time_builtins<'b>(
&self,
main_wasm: &'b [u8],
) -> Result<Cow<'b, [u8]>> {
if self.get_compile_time_builtins().is_empty() {
return Ok(main_wasm.into());
}
let imports = self.check_imports_for_compile_time_builtins(&main_wasm)?;
if imports.is_empty() {
drop(imports);
return Ok(main_wasm.into());
}
let tempdir = tempfile::TempDir::new().context("failed to create a temporary directory")?;
let deps = tempdir.path().join("_deps");
std::fs::create_dir(&deps)
.with_context(|| format!("failed to create directory: {}", deps.display()))?;
let main_wasm_path = tempdir.path().join("_main.wasm");
std::fs::write(&main_wasm_path, &main_wasm)
.with_context(|| format!("failed to write to file: {}", main_wasm_path.display()))?;
let mut config = wasm_compose::config::Config::default();
for (name, bytes) in self.get_compile_time_builtins() {
let name: &str = &*name;
if !imports.contains(&name) {
continue;
}
let mut path = deps.join(Path::new(name));
path.set_extension("wasm");
std::fs::write(&path, &bytes)
.with_context(|| format!("failed to write to file: {}", path.display()))?;
config
.dependencies
.insert(name.to_string(), wasm_compose::config::Dependency { path });
}
let composer = wasm_compose::composer::ComponentComposer::new(&main_wasm_path, &config);
let composed = composer.compose().to_wasmtime_result()?;
Ok(composed.into())
}
/// Check that the main Wasm doesn't import unsafe intrinsics, keeping the
/// TCB to just the compile-time builtins' implementation.
///
/// Returns the Wasm's top-level instance imports for `wasm-compose`
/// configuration.
fn check_imports_for_compile_time_builtins<'b>(
&self,
main_wasm: &'b [u8],
) -> Result<crate::hash_set::HashSet<&'b str>, Error> {
let intrinsics_import = self.unsafe_intrinsics_import.as_deref().ok_or_else(|| {
format_err!(
"must configure the unsafe-intrinsics import when using compile-time builtins"
)
})?;
let mut instance_imports = crate::hash_set::HashSet::new();
let parser = wasmparser::Parser::new(0);
let mut level = 0;
for payload in parser.parse_all(main_wasm) {
match payload? {
wasmparser::Payload::Version { .. } => {
level += 1;
}
wasmparser::Payload::End(_) => {
level -= 1;
}
wasmparser::Payload::ComponentImportSection(imports) if level == 1 => {
for imp in imports.into_iter() {
let imp = imp?;
// Ideally we would simply choose a new import name that
// doesn't conflict with the main Wasm's imports and
// plumb that through to the compile-time builtins
// regardless of the import name that they use, but
// unfortunately the `wasm-compose` API is not powerful
// enough for us to do all that.
ensure!(
imp.name.0 != intrinsics_import,
"main Wasm cannot import the unsafe intrinsics (`{intrinsics_import}`) \
when using compile-time builtins"
);
if let wasmparser::ComponentTypeRef::Instance(_) = imp.ty {
instance_imports.insert(imp.name.0);
}
}
}
_ => {}
}
}
Ok(instance_imports)
}
/// Define a compile-time builtin component, via its Wasm bytes.
///
/// Compile-time builtins enable you to build safe, zero-copy, and (with
/// [inlining][crate::Config::compiler_inlining])
/// zero-function-call-overhead Wasm APIs for accessing host data, buffers,
/// and objects.
///
/// A compile-time builtin is a component that is
///
/// * authored by the host (Wasmtime embedder),
///
/// * whose implementation (though not necessarily its interface!) is
/// host-specific,
///
/// * has access to unsafe intrinsics (and is therefore part of the host's
/// [trusted compute base]), and
///
/// * is linked into guest Wasm programs at compile-time.
///
/// Any imports satisfied by a compile-time builtin during compilation will
/// not show up in the resulting component's
/// [imports][crate::component::types::Component::imports], and they can no
/// longer be customized by a [`Linker`][crate::component::Linker]
/// definition at instantiation time.[^0]
///
/// [^0]: If linking compile-time builtins into a component at compile-time
/// reminds you of [component composition], that is not a coincidence:
/// component composition is used under the covers as part of compile-time
/// builtins' implementation.
///
/// Comparing compile-time builtins with
/// [`Linker`][crate::component::Linker]s is informative:
///
/// * Both mechanisms define APIs to satisfy a Wasm program's imports.
///
/// * A `Linker` satisfies those imports at instantiation-time, while
/// compile-time builtins do it during compilation.
///
/// * APIs defined by a `Linker` are implemented in Rust, and hosts can
/// build safe, sandboxed Wasm APIs on top of raw, un-sandboxed primitives
/// via Rust's `unsafe`. APIs defined by compile-time builtins are
/// implemented as Wasm components, and hosts can build safe, sandboxed
/// Wasm APIs on top of raw, un-sandboxed primitives via [unsafe
/// intrinsics][CodeBuilder::expose_unsafe_intrinsics].
///
/// * Imports satisfied via `Linker`-defined APIs are implemented with
/// [PLT/GOT]-style function table lookups and indirect calls in the
/// Wasm's compiled native code. On the other hand, Wasmtime implements
/// calls to imports satisfied via compile-time builtins with direct calls
/// in the Wasm's compiled native code. Wasmtime's compiler can also
/// [inline][crate::Config::compiler_inlining] these direct calls,
/// removing function call overheads and enabling further, cascading
/// compiler optimizations.
///
/// If you are familiar with Wasm on the Web, you can think of compile-time
/// builtins as the rough equivalent of [the `js-string-builtins` proposal]
/// but for arbitrary host-defined APIs in a Wasmtime embedding environment
/// rather than JS string APIs in a Web browser environment.
///
/// [trusted compute base]: https://en.wikipedia.org/wiki/Trusted_computing_base
/// [the `js-string-builtins` proposal]: https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md
/// [component composition]: https://component-model.bytecodealliance.org/composing-and-distributing/composing.html
/// [PLT/GOT]: https://reverseengineering.stackexchange.com/a/1993
///
/// # Safety
///
/// Compile-time builtins are part of your [trusted compute base] and should
/// be authored by trusted, first-party developers with extreme care. You
/// should never use compile-time builtins authored by untrusted,
/// third-party developers.
///
/// Compile-time builtins are given access to Wasmtime's [unsafe
/// intrinsics][CodeBuilder::expose_unsafe_intrinsics], and the same safety
/// invariants and portability concerns apply. However, when compile-time
/// builtins are defined on a `CodeBuilder`, unsafe intrinsics are *only*
/// exposed to the compile-time builtins, and they are *not* exposed to the
/// main guest Wasm program. This means that — assuming your compile-time
/// builtins only exposing safe APIs, encapsulating the intrinsics'
/// unsafety, and modulo bugs in your implementation of those safe APIs —
/// that the main guest Wasm program is not part of your trusted compute
/// base.
///
/// # Example
///
/// See the example in [CodeBuilder::expose_unsafe_intrinsics].
pub unsafe fn compile_time_builtins_binary(
&mut self,
name: impl Into<Cow<'a, str>>,
wasm_bytes: impl Into<Cow<'a, [u8]>>,
) -> &mut Self {
self.compile_time_builtins
.insert(name.into(), wasm_bytes.into());
self
}
/// Equivalent of [`CodeBuilder::compile_time_builtins_binary`] that also
/// accepts the WebAssembly text format.
///
/// This method will configure the WebAssembly binary to be compiled and
/// used to satisfy the `name` instance import. The input `wasm_bytes` may
/// either be the wasm text format or the binary format. If the `wat` crate
/// feature is enabled, which is enabled by default, then the text format
/// will automatically be converted to the binary format.
///
/// # Errors
///
/// This method will also return an error if `wasm_bytes` is the wasm text
/// format and the text syntax is not valid.
///
/// # Safety
///
/// See [`CodeBuilder::compile_time_builtins_binary`].
///
/// # Example
///
/// See the example in [CodeBuilder::expose_unsafe_intrinsics], which uses
/// compile-time builtins.
pub unsafe fn compile_time_builtins_binary_or_text(
&mut self,
name: impl Into<Cow<'a, str>>,
wasm_bytes: impl Into<Cow<'a, [u8]>>,
wasm_path: Option<&Path>,
) -> Result<&mut Self> {
let wasm_bytes = wasm_bytes.into();
#[cfg(feature = "wat")]
if let Cow::Owned(wasm_bytes) = wat::parse_bytes(&wasm_bytes).map_err(|mut e| {
if let Some(path) = wasm_path {
e.set_path(path);
}
e
})? {
// SAFETY: Same as our unsafe contract.
return Ok(unsafe { self.compile_time_builtins_binary(name, wasm_bytes) });
}
// SAFETY: Same as our unsafe contract.
Ok(unsafe { self.compile_time_builtins_binary(name, wasm_bytes) })
}
/// Like [`CodeBuilder::compile_time_builtins_binary`], but reads the `file`
/// specified for the bytes that will define the compile-time builtin.
///
/// # Safety
///
/// See [`CodeBuilder::compile_time_builtins_binary`].
///
/// # Example
///
/// See the example in [CodeBuilder::expose_unsafe_intrinsics], which uses
/// compile-time builtins.
pub unsafe fn compile_time_builtins_binary_file(
&mut self,
name: impl Into<Cow<'a, str>>,
file: &Path,
) -> Result<&mut Self> {
let wasm_bytes = std::fs::read(file)
.with_context(|| format!("failed to read file: {}", file.display()))?;
// SAFETY: Same as our unsafe contract.
Ok(unsafe { self.compile_time_builtins_binary(name, wasm_bytes) })
}
/// Equivalent of [`CodeBuilder::compile_time_builtins_binary_file`] that
/// also accepts the WebAssembly text format.
///
/// This method is will read the file at the given path and interpret the
/// contents to determine if it's the Wasm text format or binary format. The
/// file extension is not consulted. The text format is automatically
/// converted to the binary format if the crate feature `wat` is active.
///
/// # Errors
///
/// In addition to the errors returned by
/// [`CodeBuilder::compile_time_builtins_binary_file`] this may also fail if
/// the text format is read and the syntax is invalid.
///
/// # Safety
///
/// See [`CodeBuilder::compile_time_builtins_binary`].
///
/// # Example
///
/// See the example in [CodeBuilder::expose_unsafe_intrinsics], which uses
/// compile-time builtins.
pub unsafe fn compile_time_builtins_binary_or_text_file(
&mut self,
name: impl Into<Cow<'a, str>>,
file: &Path,
) -> Result<&mut Self> {
#[cfg(feature = "wat")]
{
let wasm = wat::parse_file(file)
.with_context(|| format!("error parsing file: {}", file.display()))?;
// SAFETY: Same as our unsafe contract.
Ok(unsafe { self.compile_time_builtins_binary(name, wasm) })
}
#[cfg(not(feature = "wat"))]
{
// SAFETY: Same as our unsafe contract.
unsafe { self.compile_time_builtins_binary_file(name, file) }
}
}
}