Skip to content

Commit 2f4419c

Browse files
authored
Implement runtime checks for compilation settings (#3899)
* Implement runtime checks for compilation settings This commit fills out a few FIXME annotations by implementing run-time checks that when a `Module` is created it has compatible codegen settings for the current host (as `Module` is proof of "this code can run"). This is done by implementing new `Engine`-level methods which validate compiler settings. These settings are validated on `Module::new` as well as when loading serialized modules. Settings are split into two categories, one for "shared" top-level settings and one for ISA-specific settings. Both categories now have allow-lists hardcoded into `Engine` which indicate the acceptable values for each setting (if applicable). ISA-specific settings are checked with the Rust standard library's `std::is_x86_feature_detected!` macro. Other macros for other platforms are not stable at this time but can be added here if necessary. Closes #3897 * Fix fall-through logic to actually be correct * Use a `OnceCell`, not an `AtomicBool` * Fix some broken tests
1 parent 9137b4a commit 2f4419c

File tree

7 files changed

+272
-119
lines changed

7 files changed

+272
-119
lines changed

crates/environ/src/compilation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ pub trait Compiler: Send + Sync {
235235
}
236236

237237
/// Value of a configured setting for a [`Compiler`]
238-
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq)]
238+
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug)]
239239
pub enum FlagValue {
240240
/// Name of the value that has been configured for this setting.
241241
Enum(Cow<'static, str>),

crates/wasmtime/src/engine.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use crate::signatures::SignatureRegistry;
22
use crate::{Config, Trap};
33
use anyhow::Result;
4+
use once_cell::sync::OnceCell;
45
#[cfg(feature = "parallel-compilation")]
56
use rayon::prelude::*;
67
use std::sync::atomic::{AtomicU64, Ordering};
78
use std::sync::Arc;
89
#[cfg(feature = "cache")]
910
use wasmtime_cache::CacheConfig;
11+
use wasmtime_environ::FlagValue;
1012
use wasmtime_runtime::{debug_builtins, CompiledModuleIdAllocator, InstanceAllocator};
1113

1214
/// An `Engine` which is a global context for compilation and management of wasm
@@ -44,6 +46,10 @@ struct EngineInner {
4446
signatures: SignatureRegistry,
4547
epoch: AtomicU64,
4648
unique_id_allocator: CompiledModuleIdAllocator,
49+
50+
// One-time check of whether the compiler's settings, if present, are
51+
// compatible with the native host.
52+
compatible_with_native_host: OnceCell<Result<(), String>>,
4753
}
4854

4955
impl Engine {
@@ -70,6 +76,7 @@ impl Engine {
7076
signatures: registry,
7177
epoch: AtomicU64::new(0),
7278
unique_id_allocator: CompiledModuleIdAllocator::new(),
79+
compatible_with_native_host: OnceCell::new(),
7380
}),
7481
})
7582
}
@@ -217,6 +224,211 @@ impl Engine {
217224
.map(|a| f(a))
218225
.collect::<Result<Vec<B>, E>>()
219226
}
227+
228+
/// Returns the target triple which this engine is compiling code for
229+
/// and/or running code for.
230+
pub(crate) fn target(&self) -> target_lexicon::Triple {
231+
// If a compiler is configured, use that target.
232+
#[cfg(compiler)]
233+
return self.compiler().triple().clone();
234+
235+
// ... otherwise it's the native target
236+
#[cfg(not(compiler))]
237+
return target_lexicon::Triple::host();
238+
}
239+
240+
/// Verify that this engine's configuration is compatible with loading
241+
/// modules onto the native host platform.
242+
///
243+
/// This method is used as part of `Module::new` to ensure that this
244+
/// engine can indeed load modules for the configured compiler (if any).
245+
/// Note that if cranelift is disabled this trivially returns `Ok` because
246+
/// loaded serialized modules are checked separately.
247+
pub(crate) fn check_compatible_with_native_host(&self) -> Result<()> {
248+
self.inner
249+
.compatible_with_native_host
250+
.get_or_init(|| self._check_compatible_with_native_host())
251+
.clone()
252+
.map_err(anyhow::Error::msg)
253+
}
254+
fn _check_compatible_with_native_host(&self) -> Result<(), String> {
255+
#[cfg(compiler)]
256+
{
257+
let compiler = self.compiler();
258+
259+
// Check to see that the config's target matches the host
260+
let target = compiler.triple();
261+
if *target != target_lexicon::Triple::host() {
262+
return Err(format!(
263+
"target '{}' specified in the configuration does not match the host",
264+
target
265+
));
266+
}
267+
268+
// Also double-check all compiler settings
269+
for (key, value) in compiler.flags().iter() {
270+
self.check_compatible_with_shared_flag(key, value)?;
271+
}
272+
for (key, value) in compiler.isa_flags().iter() {
273+
self.check_compatible_with_isa_flag(key, value)?;
274+
}
275+
}
276+
Ok(())
277+
}
278+
279+
/// Checks to see whether the "shared flag", something enabled for
280+
/// individual compilers, is compatible with the native host platform.
281+
///
282+
/// This is used both when validating an engine's compilation settings are
283+
/// compatible with the host as well as when deserializing modules from
284+
/// disk to ensure they're compatible with the current host.
285+
///
286+
/// Note that most of the settings here are not configured by users that
287+
/// often. While theoretically possible via `Config` methods the more
288+
/// interesting flags are the ISA ones below. Typically the values here
289+
/// represent global configuration for wasm features. Settings here
290+
/// currently rely on the compiler informing us of all settings, including
291+
/// those disabled. Settings then fall in a few buckets:
292+
///
293+
/// * Some settings must be enabled, such as `avoid_div_traps`.
294+
/// * Some settings must have a particular value, such as
295+
/// `libcall_call_conv`.
296+
/// * Some settings do not matter as to their value, such as `opt_level`.
297+
pub(crate) fn check_compatible_with_shared_flag(
298+
&self,
299+
flag: &str,
300+
value: &FlagValue,
301+
) -> Result<(), String> {
302+
let ok = match flag {
303+
// These settings must all have be enabled, since their value
304+
// can affect the way the generated code performs or behaves at
305+
// runtime.
306+
"avoid_div_traps" => *value == FlagValue::Bool(true),
307+
"unwind_info" => *value == FlagValue::Bool(true),
308+
"libcall_call_conv" => *value == FlagValue::Enum("isa_default".into()),
309+
310+
// Features wasmtime doesn't use should all be disabled, since
311+
// otherwise if they are enabled it could change the behavior of
312+
// generated code.
313+
"baldrdash_prologue_words" => *value == FlagValue::Num(0),
314+
"enable_llvm_abi_extensions" => *value == FlagValue::Bool(false),
315+
"emit_all_ones_funcaddrs" => *value == FlagValue::Bool(false),
316+
"enable_pinned_reg" => *value == FlagValue::Bool(false),
317+
"enable_probestack" => *value == FlagValue::Bool(false),
318+
"use_colocated_libcalls" => *value == FlagValue::Bool(false),
319+
"use_pinned_reg_as_heap_base" => *value == FlagValue::Bool(false),
320+
321+
// If reference types are enabled this must be enabled, otherwise
322+
// this setting can have any value.
323+
"enable_safepoints" => {
324+
if self.config().features.reference_types {
325+
*value == FlagValue::Bool(true)
326+
} else {
327+
return Ok(())
328+
}
329+
}
330+
331+
// These settings don't affect the interface or functionality of
332+
// the module itself, so their configuration values shouldn't
333+
// matter.
334+
"enable_heap_access_spectre_mitigation"
335+
| "enable_nan_canonicalization"
336+
| "enable_jump_tables"
337+
| "enable_float"
338+
| "enable_simd"
339+
| "enable_verifier"
340+
| "is_pic"
341+
| "machine_code_cfg_info"
342+
| "tls_model" // wasmtime doesn't use tls right now
343+
| "opt_level" // opt level doesn't change semantics
344+
| "probestack_func_adjusts_sp" // probestack above asserted disabled
345+
| "probestack_size_log2" // probestack above asserted disabled
346+
| "regalloc" // shouldn't change semantics
347+
| "enable_atomics" => return Ok(()),
348+
349+
// Everything else is unknown and needs to be added somewhere to
350+
// this list if encountered.
351+
_ => {
352+
return Err(format!("unknown shared setting {:?} configured to {:?}", flag, value))
353+
}
354+
};
355+
356+
if !ok {
357+
return Err(format!(
358+
"setting {:?} is configured to {:?} which is not supported",
359+
flag, value,
360+
));
361+
}
362+
Ok(())
363+
}
364+
365+
/// Same as `check_compatible_with_native_host` except used for ISA-specific
366+
/// flags. This is used to test whether a configured ISA flag is indeed
367+
/// available on the host platform itself.
368+
pub(crate) fn check_compatible_with_isa_flag(
369+
&self,
370+
flag: &str,
371+
value: &FlagValue,
372+
) -> Result<(), String> {
373+
match value {
374+
// ISA flags are used for things like CPU features, so if they're
375+
// disabled then it's compatible with the native host.
376+
FlagValue::Bool(false) => return Ok(()),
377+
378+
// Fall through below where we test at runtime that features are
379+
// available.
380+
FlagValue::Bool(true) => {}
381+
382+
// Only `bool` values are supported right now, other settings would
383+
// need more support here.
384+
_ => {
385+
return Err(format!(
386+
"isa-specific feature {:?} configured to unknown value {:?}",
387+
flag, value
388+
))
389+
}
390+
}
391+
#[cfg(target_arch = "x86_64")]
392+
{
393+
let enabled = match flag {
394+
"has_sse3" => Some(std::is_x86_feature_detected!("sse3")),
395+
"has_ssse3" => Some(std::is_x86_feature_detected!("ssse3")),
396+
"has_sse41" => Some(std::is_x86_feature_detected!("sse4.1")),
397+
"has_sse42" => Some(std::is_x86_feature_detected!("sse4.2")),
398+
"has_popcnt" => Some(std::is_x86_feature_detected!("popcnt")),
399+
"has_avx" => Some(std::is_x86_feature_detected!("avx")),
400+
"has_avx2" => Some(std::is_x86_feature_detected!("avx2")),
401+
"has_bmi1" => Some(std::is_x86_feature_detected!("bmi1")),
402+
"has_bmi2" => Some(std::is_x86_feature_detected!("bmi2")),
403+
"has_avx512bitalg" => Some(std::is_x86_feature_detected!("avx512bitalg")),
404+
"has_avx512dq" => Some(std::is_x86_feature_detected!("avx512dq")),
405+
"has_avx512f" => Some(std::is_x86_feature_detected!("avx512f")),
406+
"has_avx512vl" => Some(std::is_x86_feature_detected!("avx512vl")),
407+
"has_avx512vbmi" => Some(std::is_x86_feature_detected!("avx512vbmi")),
408+
"has_lzcnt" => Some(std::is_x86_feature_detected!("lzcnt")),
409+
410+
// fall through to the very bottom to indicate that support is
411+
// not enabled to test whether this feature is enabled on the
412+
// host.
413+
_ => None,
414+
};
415+
match enabled {
416+
Some(true) => return Ok(()),
417+
Some(false) => {
418+
return Err(format!(
419+
"compilation setting {:?} is enabled but not available on the host",
420+
flag
421+
))
422+
}
423+
// fall through
424+
None => {}
425+
}
426+
}
427+
Err(format!(
428+
"cannot test if target-specific flag {:?} is available at runtime",
429+
flag
430+
))
431+
}
220432
}
221433

222434
impl Default for Engine {

crates/wasmtime/src/module.rs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,9 @@ impl Module {
295295
#[cfg(compiler)]
296296
#[cfg_attr(nightlydoc, doc(cfg(feature = "cranelift")))] // see build.rs
297297
pub fn from_binary(engine: &Engine, binary: &[u8]) -> Result<Module> {
298-
// Check to see that the config's target matches the host
299-
let target = engine.compiler().triple();
300-
if *target != target_lexicon::Triple::host() {
301-
bail!(
302-
"target '{}' specified in the configuration does not match the host",
303-
target
304-
);
305-
}
306-
307-
// FIXME: we may want to validate that the ISA flags in the config match those that
308-
// would be inferred for the host, otherwise the JIT might produce unrunnable code
309-
// for the features the host's CPU actually has.
298+
engine
299+
.check_compatible_with_native_host()
300+
.context("compilation settings are not compatible with the native host")?;
310301

311302
cfg_if::cfg_if! {
312303
if #[cfg(feature = "cache")] {

0 commit comments

Comments
 (0)