Skip to content

Commit 10eda1c

Browse files
authored
Support platforms without 64-bit atomics (bytecodealliance#10134)
* Support platforms without 64-bit atomics This commit enables Wasmtime to build on platforms without 64-bit atomic instructions, such as Rust's `riscv32imac-unknown-none-elf` target. There are only two users of 64-bit atomics right now which are epochs and allocation of `StoreId`. This commit adds `#[cfg]` to epoch-related infrastructure in the runtime to turn that all of if 64-bit atomics aren't available. The thinking is that epochs more-or-less don't work without 64-bit atomics so it's easier to just remove them entirely. Allocation of `StoreId` is trickier though because it's so core to Wasmtime and it basically can't be removed. I've opted to change the allocator to 32-bit indices instead of 64-bit indices. Note that `StoreId` requires unique IDs to be allocated for safety which means that while a 64-bit integer won't overflow for a few thousand years a 32-bit integer will overflow in a few hours from quickly creating stores. The rough hope though is that embeddings on platforms like this aren't churning through stores. Regardless if this condition is triggered it'll result in a panic rather than unsoundness, so we hopefully have at least that going for us. Closes bytecodealliance#8768 * Update component resources to not use `AtomicU64` These aren't intended to be used in threaded contexts anyway and the use of `AtomicXXX` is just to have interior mutability while still being `Send` and `Sync`. Switch to using `AtomicU32` which is more portable. * Use `RwLock<u64>` for `StoreId` instead. * Fix compile * Fix imports
1 parent cb195e5 commit 10eda1c

File tree

14 files changed

+136
-49
lines changed

14 files changed

+136
-49
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,10 @@ jobs:
570570
apt_packages: gcc-powerpc64le-linux-gnu
571571
env:
572572
CARGO_TARGET_POWERPC64LE_UNKNOWN_LINUX_GNU_LINKER: powerpc64le-linux-gnu-gcc
573+
# A no_std target without 64-bit atomics
574+
- target: riscv32imac-unknown-none-elf
575+
os: ubuntu-latest
576+
test: cargo check -p wasmtime --no-default-features --features runtime,gc,component-model
573577
env: ${{ matrix.env || fromJSON('{}') }}
574578
steps:
575579
- uses: actions/checkout@v4

crates/environ/src/builtin.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ macro_rules! foreach_builtin_function {
4040
// Invoked when fuel has run out while executing a function.
4141
out_of_gas(vmctx: vmctx) -> bool;
4242
// Invoked when we reach a new epoch.
43+
#[cfg(target_has_atomic = "64")]
4344
new_epoch(vmctx: vmctx) -> u64;
4445
// Invoked before malloc returns.
4546
#[cfg(feature = "wmemcheck")]

crates/wasmtime/src/engine.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::runtime::vm::GcRuntime;
88
use crate::sync::OnceLock;
99
use crate::Config;
1010
use alloc::sync::Arc;
11+
#[cfg(target_has_atomic = "64")]
1112
use core::sync::atomic::{AtomicU64, Ordering};
1213
#[cfg(any(feature = "cranelift", feature = "winch"))]
1314
use object::write::{Object, StandardSegment};
@@ -61,7 +62,7 @@ struct EngineInner {
6162
profiler: Box<dyn crate::profiling_agent::ProfilingAgent>,
6263
#[cfg(feature = "runtime")]
6364
signatures: TypeRegistry,
64-
#[cfg(feature = "runtime")]
65+
#[cfg(all(feature = "runtime", target_has_atomic = "64"))]
6566
epoch: AtomicU64,
6667

6768
/// One-time check of whether the compiler's settings, if present, are
@@ -130,7 +131,7 @@ impl Engine {
130131
profiler: config.build_profiler()?,
131132
#[cfg(feature = "runtime")]
132133
signatures: TypeRegistry::new(),
133-
#[cfg(feature = "runtime")]
134+
#[cfg(all(feature = "runtime", target_has_atomic = "64"))]
134135
epoch: AtomicU64::new(0),
135136
#[cfg(any(feature = "cranelift", feature = "winch"))]
136137
compatible_with_native_host: OnceLock::new(),
@@ -318,6 +319,9 @@ impl Engine {
318319
if !cfg!(has_virtual_memory) && self.tunables().memory_init_cow {
319320
return Err("virtual memory disabled at compile time -- cannot enable CoW".into());
320321
}
322+
if !cfg!(target_has_atomic = "64") && self.tunables().epoch_interruption {
323+
return Err("epochs currently require 64-bit atomics".into());
324+
}
321325
Ok(())
322326
}
323327

@@ -692,10 +696,12 @@ impl Engine {
692696
self.config().custom_code_memory.as_ref()
693697
}
694698

699+
#[cfg(target_has_atomic = "64")]
695700
pub(crate) fn epoch_counter(&self) -> &AtomicU64 {
696701
&self.inner.epoch
697702
}
698703

704+
#[cfg(target_has_atomic = "64")]
699705
pub(crate) fn current_epoch(&self) -> u64 {
700706
self.epoch_counter().load(Ordering::Relaxed)
701707
}
@@ -725,6 +731,7 @@ impl Engine {
725731
/// This method is signal-safe: it does not make any syscalls, and
726732
/// performs only an atomic increment to the epoch value in
727733
/// memory.
734+
#[cfg(target_has_atomic = "64")]
728735
pub fn increment_epoch(&self) {
729736
self.inner.epoch.fetch_add(1, Ordering::Relaxed);
730737
}

crates/wasmtime/src/runtime/component/resources.rs

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use core::fmt;
1111
use core::marker;
1212
use core::mem::MaybeUninit;
1313
use core::ptr::NonNull;
14-
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
14+
use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
1515
use wasmtime_environ::component::{
1616
CanonicalAbiInfo, ComponentTypes, DefinedResourceIndex, InterfaceType, ResourceIndex,
1717
TypeResourceTableIndex,
@@ -248,12 +248,18 @@ pub struct Resource<T> {
248248
/// borrow tracking associated with it. The low 32-bits of the value are
249249
/// the table index and the upper 32-bits are the generation.
250250
///
251-
/// Note that this is an `AtomicU64` but it's not intended to actually be
252-
/// used in conjunction with threads as generally a `Store<T>` lives on one
253-
/// thread at a time. The `AtomicU64` here is used to ensure that this type
254-
/// is `Send + Sync` when captured as a reference to make async programming
255-
/// more ergonomic.
256-
struct AtomicResourceState(AtomicU64);
251+
/// Note that this is two `AtomicU32` fields but it's not intended to actually
252+
/// be used in conjunction with threads as generally a `Store<T>` lives on one
253+
/// thread at a time. The pair of `AtomicU32` here is used to ensure that this
254+
/// type is `Send + Sync` when captured as a reference to make async
255+
/// programming more ergonomic.
256+
///
257+
/// Also note that two `AtomicU32` here are used instead of `AtomicU64` to be
258+
/// more portable to platforms without 64-bit atomics.
259+
struct AtomicResourceState {
260+
index: AtomicU32,
261+
generation: AtomicU32,
262+
}
257263

258264
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
259265
enum ResourceState {
@@ -264,39 +270,52 @@ enum ResourceState {
264270
}
265271

266272
impl AtomicResourceState {
267-
const BORROW: Self = Self(AtomicU64::new(ResourceState::BORROW));
268-
const NOT_IN_TABLE: Self = Self(AtomicU64::new(ResourceState::NOT_IN_TABLE));
273+
const BORROW: Self = AtomicResourceState::new(ResourceState::Borrow);
274+
const NOT_IN_TABLE: Self = AtomicResourceState::new(ResourceState::NotInTable);
275+
276+
const fn new(state: ResourceState) -> AtomicResourceState {
277+
let (index, generation) = state.encode();
278+
Self {
279+
index: AtomicU32::new(index),
280+
generation: AtomicU32::new(generation),
281+
}
282+
}
269283

270284
fn get(&self) -> ResourceState {
271-
ResourceState::decode(self.0.load(Relaxed))
285+
ResourceState::decode(self.index.load(Relaxed), self.generation.load(Relaxed))
272286
}
273287

274288
fn swap(&self, state: ResourceState) -> ResourceState {
275-
ResourceState::decode(self.0.swap(state.encode(), Relaxed))
289+
let (index, generation) = state.encode();
290+
let index_prev = self.index.load(Relaxed);
291+
self.index.store(index, Relaxed);
292+
let generation_prev = self.generation.load(Relaxed);
293+
self.generation.store(generation, Relaxed);
294+
ResourceState::decode(index_prev, generation_prev)
276295
}
277296
}
278297

279298
impl ResourceState {
280299
// See comments on `state` above for info about these values.
281-
const BORROW: u64 = u64::MAX;
282-
const NOT_IN_TABLE: u64 = u64::MAX - 1;
283-
const TAKEN: u64 = u64::MAX - 2;
300+
const BORROW: u32 = u32::MAX;
301+
const NOT_IN_TABLE: u32 = u32::MAX - 1;
302+
const TAKEN: u32 = u32::MAX - 2;
284303

285-
fn decode(bits: u64) -> ResourceState {
286-
match bits {
304+
fn decode(idx: u32, generation: u32) -> ResourceState {
305+
match generation {
287306
Self::BORROW => Self::Borrow,
288307
Self::NOT_IN_TABLE => Self::NotInTable,
289308
Self::TAKEN => Self::Taken,
290-
other => Self::Index(HostResourceIndex(other)),
309+
_ => Self::Index(HostResourceIndex::new(idx, generation)),
291310
}
292311
}
293312

294-
fn encode(&self) -> u64 {
313+
const fn encode(&self) -> (u32, u32) {
295314
match self {
296-
Self::Borrow => Self::BORROW,
297-
Self::NotInTable => Self::NOT_IN_TABLE,
298-
Self::Taken => Self::TAKEN,
299-
Self::Index(index) => index.0,
315+
Self::Borrow => (0, Self::BORROW),
316+
Self::NotInTable => (0, Self::NOT_IN_TABLE),
317+
Self::Taken => (0, Self::TAKEN),
318+
Self::Index(index) => (index.index(), index.generation()),
300319
}
301320
}
302321
}
@@ -359,12 +378,12 @@ impl HostResourceIndex {
359378
HostResourceIndex(u64::from(idx) | (u64::from(generation) << 32))
360379
}
361380

362-
fn index(&self) -> u32 {
363-
u32::try_from(self.0 & 0xffffffff).unwrap()
381+
const fn index(&self) -> u32 {
382+
(self.0 & 0xffffffff) as u32
364383
}
365384

366-
fn generation(&self) -> u32 {
367-
u32::try_from(self.0 >> 32).unwrap()
385+
const fn generation(&self) -> u32 {
386+
(self.0 >> 32) as u32
368387
}
369388
}
370389

crates/wasmtime/src/runtime/store.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ pub struct StoreInner<T> {
222222

223223
limiter: Option<ResourceLimiterInner<T>>,
224224
call_hook: Option<CallHookInner<T>>,
225+
#[cfg(target_has_atomic = "64")]
225226
epoch_deadline_behavior:
226227
Option<Box<dyn FnMut(StoreContextMut<T>) -> Result<UpdateDeadline> + Send + Sync>>,
227228
// for comments about `ManuallyDrop`, see `Store::into_data`
@@ -614,6 +615,7 @@ impl<T> Store<T> {
614615
},
615616
limiter: None,
616617
call_hook: None,
618+
#[cfg(target_has_atomic = "64")]
617619
epoch_deadline_behavior: None,
618620
data: ManuallyDrop::new(data),
619621
});
@@ -995,6 +997,7 @@ impl<T> Store<T> {
995997
/// See documentation on
996998
/// [`Config::epoch_interruption()`](crate::Config::epoch_interruption)
997999
/// for an introduction to epoch-based interruption.
1000+
#[cfg(target_has_atomic = "64")]
9981001
pub fn set_epoch_deadline(&mut self, ticks_beyond_current: u64) {
9991002
self.inner.set_epoch_deadline(ticks_beyond_current);
10001003
}
@@ -1025,6 +1028,7 @@ impl<T> Store<T> {
10251028
/// See documentation on
10261029
/// [`Config::epoch_interruption()`](crate::Config::epoch_interruption)
10271030
/// for an introduction to epoch-based interruption.
1031+
#[cfg(target_has_atomic = "64")]
10281032
pub fn epoch_deadline_trap(&mut self) {
10291033
self.inner.epoch_deadline_trap();
10301034
}
@@ -1056,6 +1060,7 @@ impl<T> Store<T> {
10561060
/// See documentation on
10571061
/// [`Config::epoch_interruption()`](crate::Config::epoch_interruption)
10581062
/// for an introduction to epoch-based interruption.
1063+
#[cfg(target_has_atomic = "64")]
10591064
pub fn epoch_deadline_callback(
10601065
&mut self,
10611066
callback: impl FnMut(StoreContextMut<T>) -> Result<UpdateDeadline> + Send + Sync + 'static,
@@ -1086,7 +1091,7 @@ impl<T> Store<T> {
10861091
/// See documentation on
10871092
/// [`Config::epoch_interruption()`](crate::Config::epoch_interruption)
10881093
/// for an introduction to epoch-based interruption.
1089-
#[cfg(feature = "async")]
1094+
#[cfg(all(feature = "async", target_has_atomic = "64"))]
10901095
pub fn epoch_deadline_async_yield_and_update(&mut self, delta: u64) {
10911096
self.inner.epoch_deadline_async_yield_and_update(delta);
10921097
}
@@ -1184,13 +1189,15 @@ impl<'a, T> StoreContextMut<'a, T> {
11841189
/// Sets the epoch deadline to a certain number of ticks in the future.
11851190
///
11861191
/// For more information see [`Store::set_epoch_deadline`].
1192+
#[cfg(target_has_atomic = "64")]
11871193
pub fn set_epoch_deadline(&mut self, ticks_beyond_current: u64) {
11881194
self.0.set_epoch_deadline(ticks_beyond_current);
11891195
}
11901196

11911197
/// Configures epoch-deadline expiration to trap.
11921198
///
11931199
/// For more information see [`Store::epoch_deadline_trap`].
1200+
#[cfg(target_has_atomic = "64")]
11941201
pub fn epoch_deadline_trap(&mut self) {
11951202
self.0.epoch_deadline_trap();
11961203
}
@@ -1200,7 +1207,7 @@ impl<'a, T> StoreContextMut<'a, T> {
12001207
///
12011208
/// For more information see
12021209
/// [`Store::epoch_deadline_async_yield_and_update`].
1203-
#[cfg(feature = "async")]
1210+
#[cfg(all(feature = "async", target_has_atomic = "64"))]
12041211
pub fn epoch_deadline_async_yield_and_update(&mut self, delta: u64) {
12051212
self.0.epoch_deadline_async_yield_and_update(delta);
12061213
}
@@ -1920,7 +1927,7 @@ impl StoreOpaque {
19201927
///
19211928
/// This only works on async futures and stores, and assumes that we're
19221929
/// executing on a fiber. This will yield execution back to the caller once.
1923-
#[cfg(feature = "async")]
1930+
#[cfg(all(feature = "async", target_has_atomic = "64"))]
19241931
fn async_yield_impl(&mut self) -> Result<()> {
19251932
use crate::runtime::vm::Yield;
19261933

@@ -2688,6 +2695,7 @@ unsafe impl<T> crate::runtime::vm::VMStore for StoreInner<T> {
26882695
Ok(())
26892696
}
26902697

2698+
#[cfg(target_has_atomic = "64")]
26912699
fn new_epoch(&mut self) -> Result<u64, anyhow::Error> {
26922700
// Temporarily take the configured behavior to avoid mutably borrowing
26932701
// multiple times.
@@ -2769,6 +2777,7 @@ unsafe impl<T> crate::runtime::vm::VMStore for StoreInner<T> {
27692777
}
27702778

27712779
impl<T> StoreInner<T> {
2780+
#[cfg(target_has_atomic = "64")]
27722781
pub(crate) fn set_epoch_deadline(&mut self, delta: u64) {
27732782
// Set a new deadline based on the "epoch deadline delta".
27742783
//
@@ -2783,10 +2792,12 @@ impl<T> StoreInner<T> {
27832792
*epoch_deadline = self.engine().current_epoch() + delta;
27842793
}
27852794

2795+
#[cfg(target_has_atomic = "64")]
27862796
fn epoch_deadline_trap(&mut self) {
27872797
self.epoch_deadline_behavior = None;
27882798
}
27892799

2800+
#[cfg(target_has_atomic = "64")]
27902801
fn epoch_deadline_callback(
27912802
&mut self,
27922803
callback: Box<dyn FnMut(StoreContextMut<T>) -> Result<UpdateDeadline> + Send + Sync>,

crates/wasmtime/src/runtime/store/data.rs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use core::fmt;
55
use core::marker;
66
use core::num::NonZeroU64;
77
use core::ops::{Index, IndexMut};
8-
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
98

109
// This is defined here, in a private submodule, so we can explicitly reexport
1110
// it only as `pub(crate)`. This avoids a ton of
@@ -210,22 +209,46 @@ impl StoreId {
210209
/// Allocates a new unique identifier for a store that has never before been
211210
/// used in this process.
212211
pub fn allocate() -> StoreId {
213-
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
214-
215-
// Only allow 2^63 stores at which point we start panicking to prevent
216-
// overflow.
212+
// When 64-bit atomics are allowed then allow 2^63 stores at which point
213+
// we start panicking to prevent overflow.
217214
//
218215
// If a store is created once per microsecond then this will last the
219216
// current process for 584,540 years before overflowing.
220-
//
221-
// Also note the usage of `Relaxed` ordering here which should be ok
222-
// since we're only looking for atomicity on this counter and this
223-
// otherwise isn't used to synchronize memory stored anywhere else.
224-
let id = NEXT_ID.fetch_add(1, Relaxed);
225-
if id & (1 << 63) != 0 {
226-
NEXT_ID.store(1 << 63, Relaxed);
227-
panic!("store id allocator overflow");
228-
}
217+
const OVERFLOW_THRESHOLD: u64 = 1 << 63;
218+
219+
#[cfg(target_has_atomic = "64")]
220+
let id = {
221+
use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
222+
223+
// Note the usage of `Relaxed` ordering here which should be ok
224+
// since we're only looking for atomicity on this counter and this
225+
// otherwise isn't used to synchronize memory stored anywhere else.
226+
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
227+
let id = NEXT_ID.fetch_add(1, Relaxed);
228+
if id > OVERFLOW_THRESHOLD {
229+
NEXT_ID.store(OVERFLOW_THRESHOLD, Relaxed);
230+
panic!("store id allocator overflow");
231+
}
232+
id
233+
};
234+
235+
// When 64-bit atomics are not allowed use a `RwLock<u64>`. This is
236+
// already used elsewhere in Wasmtime and currently has the
237+
// implementation of panic-on-contention, but it's at least no worse
238+
// than what wasmtime had before and is at least correct and UB-free.
239+
#[cfg(not(target_has_atomic = "64"))]
240+
let id = {
241+
use crate::sync::RwLock;
242+
static NEXT_ID: RwLock<u64> = RwLock::new(0);
243+
244+
let mut lock = NEXT_ID.write();
245+
if *lock > OVERFLOW_THRESHOLD {
246+
panic!("store id allocator overflow");
247+
}
248+
let ret = *lock;
249+
*lock += 1;
250+
ret
251+
};
229252

230253
StoreId(NonZeroU64::new(id + 1).unwrap())
231254
}

crates/wasmtime/src/runtime/vm.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ pub unsafe trait VMStore {
178178
/// Callback invoked whenever an instance observes a new epoch
179179
/// number. Cannot fail; cooperative epoch-based yielding is
180180
/// completely semantically transparent. Returns the new deadline.
181+
#[cfg(target_has_atomic = "64")]
181182
fn new_epoch(&mut self) -> Result<u64, Error>;
182183

183184
/// Callback invoked whenever an instance needs to trigger a GC.

0 commit comments

Comments
 (0)