Skip to content

Commit c7dc2d5

Browse files
authored
Disallow loading code on x86_64-unknown-none (#11553)
* Disallow loading code on `x86_64-unknown-none` ... And then also add an escape hatch to allow loading code. This commit is the culmination of discussion on #11506 with a proposed resolution for Wasmtime. The resolution being: * Wasmtime will reject loading code on `x86_64-unknown-none` platforms by default. * A new `Config::x86_float_abi_ok` escape hatch is added to bypass this check. * Documentation/errors are updated around `x86_float_abi_ok` to document the situation. * The `min-platform` example is updated to showcase how this is valid to run in that particular embedding (aka enable more features and sufficiently detect said features). The basic tl;dr; is that we can't detect in stable Rust what float ABI is being used so therefore we pessimistically assume that `x86_64-unknown-none` is using a soft-float ABI. This is incompatible with libcalls unless they aren't actually called which is only possible when sufficiently many target features are enabled. The goal of this commit is to be a relatively low-effort way to place a roadblock in the way of "ok ABIs are weird" but at the same time enable getting around the roadblock easily. Additionally the roadblock points to documentation about itself to learn more about what's going on here. Closes #11506 * Add audit of raw-cpuid * Add back in check Turns out it doesn't go through the same path as other bits * Review comments * Fix running floats without without custom support
1 parent 4c39cb9 commit c7dc2d5

File tree

8 files changed

+229
-26
lines changed

8 files changed

+229
-26
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/wasmtime/src/config.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ pub struct Config {
163163
pub(crate) coredump_on_trap: bool,
164164
pub(crate) macos_use_mach_ports: bool,
165165
pub(crate) detect_host_feature: Option<fn(&str) -> Option<bool>>,
166+
pub(crate) x86_float_abi_ok: Option<bool>,
166167
}
167168

168169
/// User-provided configuration for the compiler.
@@ -271,6 +272,7 @@ impl Config {
271272
detect_host_feature: Some(detect_host_feature),
272273
#[cfg(not(feature = "std"))]
273274
detect_host_feature: None,
275+
x86_float_abi_ok: None,
274276
};
275277
#[cfg(any(feature = "cranelift", feature = "winch"))]
276278
{
@@ -2701,6 +2703,76 @@ impl Config {
27012703
pub fn gc_support(&mut self, enable: bool) -> &mut Self {
27022704
self.wasm_feature(WasmFeatures::GC_TYPES, enable)
27032705
}
2706+
2707+
/// Explicitly indicate or not whether the host is using a hardware float
2708+
/// ABI on x86 targets.
2709+
///
2710+
/// This configuration option is only applicable on the
2711+
/// `x86_64-unknown-none` Rust target and has no effect on other host
2712+
/// targets. The `x86_64-unknown-none` Rust target does not support hardware
2713+
/// floats by default and uses a "soft float" implementation and ABI. This
2714+
/// means that `f32`, for example, is passed in a general-purpose register
2715+
/// between functions instead of a floating-point register. This does not
2716+
/// match Cranelift's ABI for `f32` where it's passed in floating-point
2717+
/// registers. Cranelift does not have support for a "soft float"
2718+
/// implementation where all floating-point operations are lowered to
2719+
/// libcalls.
2720+
///
2721+
/// This means that for the `x86_64-unknown-none` target the ABI between
2722+
/// Wasmtime's libcalls and the host is incompatible when floats are used.
2723+
/// This further means that, by default, Wasmtime is unable to load native
2724+
/// code when compiled to the `x86_64-unknown-none` target. The purpose of
2725+
/// this option is to explicitly allow loading code and bypass this check.
2726+
///
2727+
/// Setting this configuration option to `true` indicates that either:
2728+
/// (a) the Rust target is compiled with the hard-float ABI manually via
2729+
/// `-Zbuild-std` and a custom target JSON configuration, or (b) sufficient
2730+
/// x86 features have been enabled in the compiler such that float libcalls
2731+
/// will not be used in Wasmtime. For (a) there is no way in Rust at this
2732+
/// time to detect whether a hard-float or soft-float ABI is in use on
2733+
/// stable Rust, so this manual opt-in is required. For (b) the only
2734+
/// instance where Wasmtime passes a floating-point value in a register
2735+
/// between the host and compiled wasm code is with libcalls.
2736+
///
2737+
/// Float-based libcalls are only used when the compilation target for a
2738+
/// wasm module has insufficient target features enabled for native
2739+
/// support. For example SSE4.1 is required for the `f32.ceil` WebAssembly
2740+
/// instruction to be compiled to a native instruction. If SSE4.1 is not
2741+
/// enabled then `f32.ceil` is translated to a "libcall" which is
2742+
/// implemented on the host. Float-based libcalls can be avoided with
2743+
/// sufficient target features enabled, for example:
2744+
///
2745+
/// * `self.cranelift_flag_enable("has_sse3")`
2746+
/// * `self.cranelift_flag_enable("has_ssse3")`
2747+
/// * `self.cranelift_flag_enable("has_sse41")`
2748+
/// * `self.cranelift_flag_enable("has_sse42")`
2749+
/// * `self.cranelift_flag_enable("has_fma")`
2750+
///
2751+
/// Note that when these features are enabled Wasmtime will perform a
2752+
/// runtime check to determine that the host actually has the feature
2753+
/// present.
2754+
///
2755+
/// For some more discussion see [#11506].
2756+
///
2757+
/// [#11506]: https://github.com/bytecodealliance/wasmtime/issues/11506
2758+
///
2759+
/// # Safety
2760+
///
2761+
/// This method is not safe because it cannot be detected in Rust right now
2762+
/// whether the host is compiled with a soft or hard float ABI. Additionally
2763+
/// if the host is compiled with a soft float ABI disabling this check does
2764+
/// not ensure that the wasm module in question has zero usage of floats
2765+
/// in the boundary to the host.
2766+
///
2767+
/// Safely using this method requires one of:
2768+
///
2769+
/// * The host target is compiled to use hardware floats.
2770+
/// * Wasm modules loaded are compiled with enough x86 Cranelift features
2771+
/// enabled to avoid float-related hostcalls.
2772+
pub unsafe fn x86_float_abi_ok(&mut self, enable: bool) -> &mut Self {
2773+
self.x86_float_abi_ok = Some(enable);
2774+
self
2775+
}
27042776
}
27052777

27062778
impl Default for Config {

crates/wasmtime/src/engine.rs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ struct EngineInner {
6565

6666
/// One-time check of whether the compiler's settings, if present, are
6767
/// compatible with the native host.
68-
#[cfg(any(feature = "cranelift", feature = "winch"))]
6968
compatible_with_native_host: crate::sync::OnceLock<Result<(), String>>,
7069
}
7170

@@ -141,7 +140,6 @@ impl Engine {
141140
signatures: TypeRegistry::new(),
142141
#[cfg(all(feature = "runtime", target_has_atomic = "64"))]
143142
epoch: AtomicU64::new(0),
144-
#[cfg(any(feature = "cranelift", feature = "winch"))]
145143
compatible_with_native_host: Default::default(),
146144
config,
147145
tunables,
@@ -293,7 +291,6 @@ impl Engine {
293291
/// engine can indeed load modules for the configured compiler (if any).
294292
/// Note that if cranelift is disabled this trivially returns `Ok` because
295293
/// loaded serialized modules are checked separately.
296-
#[cfg(any(feature = "cranelift", feature = "winch"))]
297294
pub(crate) fn check_compatible_with_native_host(&self) -> Result<()> {
298295
self.inner
299296
.compatible_with_native_host
@@ -302,18 +299,16 @@ impl Engine {
302299
.map_err(anyhow::Error::msg)
303300
}
304301

305-
#[cfg(any(feature = "cranelift", feature = "winch"))]
306302
fn _check_compatible_with_native_host(&self) -> Result<(), String> {
307303
use target_lexicon::Triple;
308304

309-
let compiler = self.compiler();
310-
311-
let target = compiler.triple();
312305
let host = Triple::host();
306+
let target = self.config().compiler_target();
307+
313308
let target_matches_host = || {
314309
// If the host target and target triple match, then it's valid
315310
// to run results of compilation on this host.
316-
if host == *target {
311+
if host == target {
317312
return true;
318313
}
319314

@@ -337,12 +332,16 @@ impl Engine {
337332
));
338333
}
339334

340-
// Also double-check all compiler settings
341-
for (key, value) in compiler.flags().iter() {
342-
self.check_compatible_with_shared_flag(key, value)?;
343-
}
344-
for (key, value) in compiler.isa_flags().iter() {
345-
self.check_compatible_with_isa_flag(key, value)?;
335+
#[cfg(any(feature = "cranelift", feature = "winch"))]
336+
{
337+
let compiler = self.compiler();
338+
// Also double-check all compiler settings
339+
for (key, value) in compiler.flags().iter() {
340+
self.check_compatible_with_shared_flag(key, value)?;
341+
}
342+
for (key, value) in compiler.isa_flags().iter() {
343+
self.check_compatible_with_isa_flag(key, value)?;
344+
}
346345
}
347346

348347
// Double-check that this configuration isn't requesting capabilities
@@ -356,6 +355,22 @@ impl Engine {
356355
if !cfg!(target_has_atomic = "64") && self.tunables().epoch_interruption {
357356
return Err("epochs currently require 64-bit atomics".into());
358357
}
358+
359+
// Double-check that the host's float ABI matches Cranelift's float ABI.
360+
// See `Config::x86_float_abi_ok` for some more
361+
// information.
362+
if target == target_lexicon::triple!("x86_64-unknown-none")
363+
&& self.config().x86_float_abi_ok != Some(true)
364+
{
365+
return Err("\
366+
the x86_64-unknown-none target by default uses a soft-float ABI that is \
367+
incompatible with Cranelift and Wasmtime -- use \
368+
`Config::x86_float_abi_ok` to disable this check and see more \
369+
information about this check\
370+
"
371+
.into());
372+
}
373+
359374
Ok(())
360375
}
361376

@@ -601,8 +616,8 @@ impl Engine {
601616
available on the host",
602617
)),
603618
None => Err(format!(
604-
"failed to detect if target-specific flag {flag:?} is \
605-
available at runtime"
619+
"failed to detect if target-specific flag {host_feature:?} is \
620+
available at runtime (compile setting {flag:?})"
606621
)),
607622
}
608623
}
@@ -876,6 +891,9 @@ impl Engine {
876891
mmap: crate::runtime::vm::MmapVec,
877892
expected: ObjectKind,
878893
) -> Result<Arc<crate::CodeMemory>> {
894+
self.check_compatible_with_native_host()
895+
.context("compilation settings are not compatible with the native host")?;
896+
879897
serialization::check_compatible(self, &mmap, expected)?;
880898
let mut code = crate::CodeMemory::new(self, mmap)?;
881899
code.publish()?;

examples/min-platform/embedding/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ wasmtime-wasi-io = { workspace = true, optional = true }
2222
# Memory allocator used in this example (not required, however)
2323
dlmalloc = "0.2.4"
2424

25+
[target.'cfg(target_arch = "x86_64")'.dependencies]
26+
raw-cpuid = "11.5.0"
27+
2528
[lib]
2629
crate-type = ['staticlib']
2730
test = false

examples/min-platform/embedding/src/lib.rs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
extern crate alloc;
55

66
use alloc::string::ToString;
7-
use anyhow::Result;
7+
use anyhow::{Result, ensure};
88
use core::ptr;
9-
use wasmtime::{Engine, Instance, Linker, Module, Store};
9+
use wasmtime::{Config, Engine, Instance, Linker, Module, Store};
1010

1111
mod allocator;
1212
mod panic;
@@ -29,14 +29,17 @@ pub unsafe extern "C" fn run(
2929
simple_add_size: usize,
3030
simple_host_fn_module: *const u8,
3131
simple_host_fn_size: usize,
32+
simple_floats_module: *const u8,
33+
simple_floats_size: usize,
3234
) -> usize {
3335
unsafe {
3436
let buf = core::slice::from_raw_parts_mut(error_buf, error_size);
3537
let smoke = core::slice::from_raw_parts(smoke_module, smoke_size);
3638
let simple_add = core::slice::from_raw_parts(simple_add_module, simple_add_size);
3739
let simple_host_fn =
3840
core::slice::from_raw_parts(simple_host_fn_module, simple_host_fn_size);
39-
match run_result(smoke, simple_add, simple_host_fn) {
41+
let simple_floats = core::slice::from_raw_parts(simple_floats_module, simple_floats_size);
42+
match run_result(smoke, simple_add, simple_host_fn, simple_floats) {
4043
Ok(()) => 0,
4144
Err(e) => {
4245
let msg = format!("{e:?}");
@@ -52,15 +55,58 @@ fn run_result(
5255
smoke_module: &[u8],
5356
simple_add_module: &[u8],
5457
simple_host_fn_module: &[u8],
58+
simple_floats_module: &[u8],
5559
) -> Result<()> {
5660
smoke(smoke_module)?;
5761
simple_add(simple_add_module)?;
5862
simple_host_fn(simple_host_fn_module)?;
63+
simple_floats(simple_floats_module)?;
5964
Ok(())
6065
}
6166

67+
fn config() -> Config {
68+
let mut config = Config::new();
69+
let _ = &mut config;
70+
71+
#[cfg(target_arch = "x86_64")]
72+
{
73+
// This example runs in a Linux process where it's valid to use
74+
// floating point registers. Additionally sufficient x86 features are
75+
// enabled during compilation to avoid float-related libcalls. Thus
76+
// despite the host being configured for "soft float" it should be
77+
// valid to turn this on.
78+
unsafe {
79+
config.x86_float_abi_ok(true);
80+
}
81+
82+
// To make the float ABI above OK it requires CPU features above
83+
// baseline to be enabled. Wasmtime needs to be able to check to ensure
84+
// that the feature is actually supplied at runtime, but a default check
85+
// isn't possible in no_std. For x86_64 we can use the cpuid instruction
86+
// bound through an external crate.
87+
//
88+
// Note that CPU support for these features has existend since 2013
89+
// (Haswell) on Intel chips and 2012 (Piledriver) on AMD chips.
90+
unsafe {
91+
config.detect_host_feature(move |feature| {
92+
let id = raw_cpuid::CpuId::new();
93+
match feature {
94+
"sse3" => Some(id.get_feature_info()?.has_sse3()),
95+
"ssse3" => Some(id.get_feature_info()?.has_sse3()),
96+
"sse4.1" => Some(id.get_feature_info()?.has_sse41()),
97+
"sse4.2" => Some(id.get_feature_info()?.has_sse42()),
98+
"fma" => Some(id.get_feature_info()?.has_fma()),
99+
_ => None,
100+
}
101+
});
102+
}
103+
}
104+
105+
config
106+
}
107+
62108
fn smoke(module: &[u8]) -> Result<()> {
63-
let engine = Engine::default();
109+
let engine = Engine::new(&config())?;
64110
let module = match deserialize(&engine, module)? {
65111
Some(module) => module,
66112
None => return Ok(()),
@@ -70,20 +116,20 @@ fn smoke(module: &[u8]) -> Result<()> {
70116
}
71117

72118
fn simple_add(module: &[u8]) -> Result<()> {
73-
let engine = Engine::default();
119+
let engine = Engine::new(&config())?;
74120
let module = match deserialize(&engine, module)? {
75121
Some(module) => module,
76122
None => return Ok(()),
77123
};
78124
let mut store = Store::new(&engine, ());
79125
let instance = Linker::new(&engine).instantiate(&mut store, &module)?;
80126
let func = instance.get_typed_func::<(u32, u32), u32>(&mut store, "add")?;
81-
assert_eq!(func.call(&mut store, (2, 3))?, 5);
127+
ensure!(func.call(&mut store, (2, 3))? == 5);
82128
Ok(())
83129
}
84130

85131
fn simple_host_fn(module: &[u8]) -> Result<()> {
86-
let engine = Engine::default();
132+
let engine = Engine::new(&config())?;
87133
let module = match deserialize(&engine, module)? {
88134
Some(module) => module,
89135
None => return Ok(()),
@@ -93,7 +139,20 @@ fn simple_host_fn(module: &[u8]) -> Result<()> {
93139
let mut store = Store::new(&engine, ());
94140
let instance = linker.instantiate(&mut store, &module)?;
95141
let func = instance.get_typed_func::<(u32, u32, u32), u32>(&mut store, "add_and_mul")?;
96-
assert_eq!(func.call(&mut store, (2, 3, 4))?, 10);
142+
ensure!(func.call(&mut store, (2, 3, 4))? == 10);
143+
Ok(())
144+
}
145+
146+
fn simple_floats(module: &[u8]) -> Result<()> {
147+
let engine = Engine::new(&config())?;
148+
let module = match deserialize(&engine, module)? {
149+
Some(module) => module,
150+
None => return Ok(()),
151+
};
152+
let mut store = Store::new(&engine, ());
153+
let instance = Linker::new(&engine).instantiate(&mut store, &module)?;
154+
let func = instance.get_typed_func::<(f32, f32), f32>(&mut store, "frob")?;
155+
ensure!(func.call(&mut store, (1.4, 3.2))? == 5.);
97156
Ok(())
98157
}
99158

examples/min-platform/embedding/src/wasi.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use core::future::Future;
3333
use core::pin::Pin;
3434
use core::task::{Context, Poll, Waker};
3535
use wasmtime::component::{Component, Linker, Resource, ResourceTable};
36-
use wasmtime::{Config, Engine, Store};
36+
use wasmtime::{Engine, Store};
3737
use wasmtime_wasi_io::{
3838
IoView,
3939
bytes::Bytes,
@@ -78,7 +78,7 @@ fn run(wasi_component: &[u8]) -> Result<String> {
7878
// interface will poll as Pending while execution is suspended and it is
7979
// waiting for a Pollable to become Ready. This example provides a very
8080
// small async executor which is entered below with `block_on`.
81-
let mut config = Config::default();
81+
let mut config = super::config();
8282
config.async_support(true);
8383
// For future: we could consider turning on fuel in the Config to meter
8484
// how long a wasm guest could execute for.

0 commit comments

Comments
 (0)