wasmtime_environ/trap_encoding.rs
1use crate::bytes;
2use core::fmt;
3use object::{Bytes, LittleEndian, U32};
4
5/// Information about trap.
6#[derive(Debug, PartialEq, Eq, Clone)]
7pub struct TrapInformation {
8 /// The offset of the trapping instruction in native code.
9 ///
10 /// This is relative to the beginning of the function.
11 pub code_offset: u32,
12
13 /// Code of the trap.
14 pub trap_code: CompiledTrap,
15}
16
17/// Possible traps that can be compiled into WebAssembly modules.
18#[derive(Debug, PartialEq, Eq, Clone)]
19pub enum CompiledTrap {
20 /// A normal trap, expected to possibly be hit at runtime.
21 Normal(Trap),
22 /// An internal assertion in the compiled code itself.
23 InternalAssert,
24 /// The GC heap was detected as being corrupt.
25 GcHeapCorrupt,
26}
27
28impl CompiledTrap {
29 /// Encodes this as a byte.
30 pub fn as_u8(&self) -> u8 {
31 match self {
32 CompiledTrap::Normal(trap) => {
33 let ret = *trap as u8;
34 debug_assert_ne!(ret, CompiledTrap::InternalAssert.as_u8());
35 debug_assert_ne!(ret, CompiledTrap::GcHeapCorrupt.as_u8());
36 ret
37 }
38 CompiledTrap::InternalAssert => 0xFF,
39 CompiledTrap::GcHeapCorrupt => 0xFE,
40 }
41 }
42
43 /// Decodes a byte as a trap.
44 pub fn from_u8(byte: u8) -> Option<CompiledTrap> {
45 if byte == 0xFF {
46 Some(CompiledTrap::InternalAssert)
47 } else if byte == 0xFE {
48 Some(CompiledTrap::GcHeapCorrupt)
49 } else {
50 Trap::from_u8(byte).map(CompiledTrap::Normal)
51 }
52 }
53}
54
55impl From<Trap> for CompiledTrap {
56 fn from(trap: Trap) -> CompiledTrap {
57 CompiledTrap::Normal(trap)
58 }
59}
60
61macro_rules! generate_trap_type {
62 (pub enum Trap {
63 $(
64 $(#[$doc:meta])*
65 $name:ident = $msg:tt,
66 )*
67 }) => {
68 #[non_exhaustive]
69 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
70 #[expect(missing_docs, reason = "self-describing variants")]
71 pub enum Trap {
72 $(
73 $(#[$doc])*
74 $name,
75 )*
76 }
77
78 impl Trap {
79 /// Converts a byte back into a `Trap` if its in-bounds
80 pub fn from_u8(byte: u8) -> Option<Trap> {
81 $(
82 if byte == Trap::$name as u8 {
83 return Some(Trap::$name);
84 }
85 )*
86 None
87 }
88 }
89
90 impl fmt::Display for Trap {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 let desc = match self {
93 $(Self::$name => $msg,)*
94 };
95 write!(f, "wasm trap: {desc}")
96 }
97 }
98 }
99}
100
101// The code can be accessed from the c-api, where the possible values are
102// translated into enum values defined there:
103//
104// * the const assertions in c-api/src/trap.rs, and
105// * `wasmtime_trap_code_enum` in c-api/include/wasmtime/trap.h.
106//
107// These need to be kept in sync.
108generate_trap_type! {
109 pub enum Trap {
110 /// The current stack space was exhausted.
111 StackOverflow = "call stack exhausted",
112
113 /// An out-of-bounds memory access.
114 MemoryOutOfBounds = "out of bounds memory access",
115
116 /// A wasm atomic operation was presented with a not-naturally-aligned linear-memory address.
117 HeapMisaligned = "unaligned atomic",
118
119 /// An out-of-bounds access to a table.
120 TableOutOfBounds = "undefined element: out of bounds table access",
121
122 /// Indirect call to a null table entry.
123 IndirectCallToNull = "uninitialized element",
124
125 /// Signature mismatch on indirect call.
126 BadSignature = "indirect call type mismatch",
127
128 /// An integer arithmetic operation caused an overflow.
129 IntegerOverflow = "integer overflow",
130
131 /// An integer division by zero.
132 IntegerDivisionByZero = "integer divide by zero",
133
134 /// Failed float-to-int conversion.
135 BadConversionToInteger = "invalid conversion to integer",
136
137 /// Code that was supposed to have been unreachable was reached.
138 UnreachableCodeReached = "wasm `unreachable` instruction executed",
139
140 /// Execution has potentially run too long and may be interrupted.
141 Interrupt = "interrupt",
142
143 /// When wasm code is configured to consume fuel and it runs out of fuel
144 /// then this trap will be raised.
145 OutOfFuel = "all fuel consumed by WebAssembly",
146
147 /// Used to indicate that a trap was raised by atomic wait operations on non shared memory.
148 AtomicWaitNonSharedMemory = "atomic wait on non-shared memory",
149
150 /// Call to a null reference.
151 NullReference = "null reference",
152
153 /// Attempt to access beyond the bounds of an array.
154 ArrayOutOfBounds = "out of bounds array access",
155
156 /// Attempted an allocation that was too large to succeed.
157 AllocationTooLarge = "allocation size too large",
158
159 /// Attempted to cast a reference to a type that it is not an instance of.
160 CastFailure = "cast failure",
161
162 /// When the `component-model` feature is enabled this trap represents a
163 /// scenario where one component tried to call another component but it
164 /// would have violated the reentrance rules of the component model,
165 /// triggering a trap instead.
166 CannotEnterComponent = "cannot enter component instance",
167
168 /// Async-lifted export failed to produce a result by calling `task.return`
169 /// before returning `STATUS_DONE` and/or after all host tasks completed.
170 NoAsyncResult = "async-lifted export failed to produce a result",
171
172 /// We are suspending to a tag for which there is no active handler.
173 UnhandledTag = "unhandled tag",
174
175 /// Attempt to resume a continuation twice.
176 ContinuationAlreadyConsumed = "continuation already consumed",
177
178 /// A Pulley opcode was executed at runtime when the opcode was disabled at
179 /// compile time.
180 DisabledOpcode = "pulley opcode disabled at compile time was executed",
181
182 /// Async event loop deadlocked; i.e. it cannot make further progress given
183 /// that all host tasks have completed and any/all host-owned stream/future
184 /// handles have been dropped.
185 AsyncDeadlock = "deadlock detected: event loop cannot make further progress",
186
187 /// When the `component-model` feature is enabled this trap represents a
188 /// scenario where a component instance tried to call an import or intrinsic
189 /// when it wasn't allowed to, e.g. from a post-return function.
190 CannotLeaveComponent = "cannot leave component instance",
191
192 /// A synchronous task attempted to make a potentially blocking call prior
193 /// to returning.
194 CannotBlockSyncTask = "cannot block a synchronous task before returning",
195
196 /// A component tried to lift a `char` with an invalid bit pattern.
197 InvalidChar = "invalid `char` bit pattern",
198
199 /// Debug assertion generated for a fused adapter regarding the expected
200 /// completion of a string encoding operation.
201 DebugAssertStringEncodingFinished = "should have finished string encoding",
202
203 /// Debug assertion generated for a fused adapter regarding a string
204 /// encoding operation.
205 DebugAssertEqualCodeUnits = "code units should be equal",
206
207 /// Debug assertion generated for a fused adapter regarding the alignment of
208 /// a pointer.
209 DebugAssertPointerAligned = "pointer should be aligned",
210
211 /// Debug assertion generated for a fused adapter regarding the upper bits
212 /// of a 64-bit value.
213 DebugAssertUpperBitsUnset = "upper bits should be unset",
214
215 /// A component tried to lift or lower a string past the end of its memory.
216 StringOutOfBounds = "string content out-of-bounds",
217
218 /// A component tried to lift or lower a list past the end of its memory.
219 ListOutOfBounds = "list content out-of-bounds",
220
221 /// A component used an invalid discriminant when lowering a variant value.
222 InvalidDiscriminant = "invalid variant discriminant",
223
224 /// A component passed an unaligned pointer when lifting or lowering a
225 /// value.
226 UnalignedPointer = "unaligned pointer",
227
228 /// `task.cancel` was called by a task which has not been cancelled.
229 TaskCancelNotCancelled = "`task.cancel` called by task which has not been cancelled",
230
231 /// `task.return` or `task.cancel` was called more than once for the
232 /// current task.
233 TaskCancelOrReturnTwice = "`task.return` or `task.cancel` called more than once for current task",
234
235 /// `subtask.cancel` was called after terminal status was already
236 /// delivered.
237 SubtaskCancelAfterTerminal = "`subtask.cancel` called after terminal status delivered",
238
239 /// Invalid `task.return` signature and/or options for the current task.
240 TaskReturnInvalid = "invalid `task.return` signature and/or options for current task",
241
242 /// Cannot drop waitable set with waiters in it.
243 WaitableSetDropHasWaiters = "cannot drop waitable set with waiters",
244
245 /// Cannot drop a subtask which has not yet resolved.
246 SubtaskDropNotResolved = "cannot drop a subtask which has not yet resolved",
247
248 /// Start function does not match the expected type.
249 ThreadNewIndirectInvalidType = "start function does not match expected type (currently only `(i32) -> ()` is supported)",
250
251 /// The start function index points to an uninitialized function.
252 ThreadNewIndirectUninitialized = "the start function index points to an uninitialized function",
253
254 /// Backpressure-related intrinsics overflowed the built-in 16-bit
255 /// counter.
256 BackpressureOverflow = "backpressure counter overflow",
257
258 /// Invalid code returned from `callback` of `async`-lifted function.
259 UnsupportedCallbackCode = "unsupported callback code",
260
261 /// Cannot resume a thread which is not suspended.
262 CannotResumeThread = "cannot resume thread which is not suspended",
263
264 /// Cannot issue a read/write on a future/stream while there is a
265 /// pending operation already.
266 ConcurrentFutureStreamOp = "cannot have concurrent operations active on a future/stream",
267
268 /// A reference count (for e.g. an `error-context`) overflowed.
269 ReferenceCountOverflow = "reference count overflow",
270
271 /// A read/write on a stream must be <2**28 items.
272 StreamOpTooBig = "stream read/write count too large",
273
274 /// The guest either attempted to add a waitable to a waitable set while
275 /// it was being used in a synchronous operation or tried to use it in a
276 /// synchronous operation while it was added to a waitable set.
277 WaitableSyncAndAsync = "waitable cannot be used synchronously while added to a waitable set",
278
279 /// An exception propagated out of a component without being caught.
280 UncaughtException = "uncaught exception propagated out of component",
281
282 /// Guest attempted to read from a stream after being notified that the
283 /// writable end was dropped.
284 ReadFromDroppedStream = "cannot read from stream after being notified that the writable end dropped",
285
286 /// Guest attempted to write to a stream after being notified that the
287 /// readable end was dropped.
288 WriteToDroppedStream = "cannot write to stream after being notified that the readable end dropped",
289
290 /// Guest attempted to write to a future after being notified that the
291 /// readable end was dropped.
292 WriteToDroppedFuture = "cannot write to future after previous write succeeded or readable end dropped",
293
294 /// Guest attempted to lift a stream after being notified that the
295 /// writable end was dropped.
296 LiftDroppedStream = "cannot lift stream after being notified that the writable end dropped",
297
298 // if adding a variant here be sure to update `trap.rs` and `trap.h` as
299 // mentioned above
300 }
301}
302
303impl core::error::Error for Trap {}
304
305/// Number of trap entries packed into one block of the trap section.
306///
307/// See `TrapEncodingBuilder` in `crate::compile` for the full section format.
308/// Chosen as a balance between the fixed-width index overhead per block (8
309/// bytes, amortized across entries) and the amount of linear decoding required
310/// to look up a single pc within a block.
311pub(crate) const TRAP_BLOCK_SIZE: usize = 128;
312
313/// Decodes the provided trap information section and attempts to find the trap
314/// code corresponding to the `offset` specified.
315///
316/// The `section` provided is expected to have been built by
317/// `TrapEncodingBuilder` in `crate::compile`, whose documentation describes
318/// the format decoded here. Additionally the `offset` should be a relative
319/// offset within the text section of the compilation image.
320pub fn lookup_trap_code(section: &[u8], offset: usize) -> Option<CompiledTrap> {
321 let section = parse(section)?;
322 let offset = u32::try_from(offset).ok()?;
323
324 // Find the last block whose first pc is `<= offset`; only that block can
325 // contain `offset`. Note that this is a precise search because trap pcs
326 // should always be precise as well as our metadata about them, which means
327 // we expect an exact match to correspond to a trap opcode.
328 let block = section
329 .block_index
330 .partition_point(|[first_offset, _]| first_offset.get(LittleEndian) <= offset)
331 .checked_sub(1)?;
332
333 for (pc, byte) in section.block_entries(block)? {
334 if pc == offset {
335 let trap = CompiledTrap::from_u8(byte);
336 debug_assert!(trap.is_some(), "missing mapping for {byte}");
337 return trap;
338 }
339 if pc > offset {
340 break;
341 }
342 }
343 None
344}
345
346/// A parsed view of the trap section.
347///
348/// The fields here correspond to the pieces of the section layout described
349/// on `TrapEncodingBuilder` in `crate::compile`.
350#[derive(Clone, Copy)]
351struct TrapSection<'a> {
352 /// Total number of trap entries in this section.
353 entries: usize,
354 /// One `(first_offset, block_pos)` pair per block.
355 block_index: &'a [[U32<LittleEndian>; 2]],
356 /// Variable-length block bodies, index by `block_pos` in the `block_index`
357 /// table above.
358 block_bodies: &'a [u8],
359}
360
361impl<'a> TrapSection<'a> {
362 /// Returns an iterator of `(text_offset, trap_code_byte)` for all entries
363 /// in `block`, or `None` if the section is malformed.
364 fn block_entries(&self, block_index: usize) -> Option<BlockEntries<'a>> {
365 let [first_offset, block_pos] = self.block_index.get(block_index)?;
366 let first_offset = first_offset.get(LittleEndian);
367 let block_pos = block_pos.get(LittleEndian);
368 let mut block = self.block_bodies.get(usize::try_from(block_pos).ok()?..)?;
369 let default_code = bytes::pop(&mut block)?;
370 let remaining = core::cmp::min(
371 TRAP_BLOCK_SIZE,
372 self.entries.checked_sub(block_index * TRAP_BLOCK_SIZE)?,
373 );
374 Some(BlockEntries {
375 block,
376 prev_offset: first_offset,
377 default_code,
378 remaining,
379 })
380 }
381}
382
383/// Iterator over the entries of a single block, decoding the
384/// delta-and-code-flag varints described in the "block body" portion of the
385/// section format on `TrapEncodingBuilder` in `crate::compile`.
386struct BlockEntries<'a> {
387 block: &'a [u8],
388 prev_offset: u32,
389 default_code: u8,
390 remaining: usize,
391}
392
393impl Iterator for BlockEntries<'_> {
394 type Item = (u32, u8);
395
396 fn next(&mut self) -> Option<(u32, u8)> {
397 self.remaining = self.remaining.checked_sub(1)?;
398 let token = bytes::read_uleb(&mut self.block)?;
399 let delta = u32::try_from(token >> 1).ok()?;
400 let cur_offset = self.prev_offset.checked_add(delta)?;
401 self.prev_offset = cur_offset;
402 let code = if token & 1 != 0 {
403 bytes::pop(&mut self.block)?
404 } else {
405 self.default_code
406 };
407 Some((cur_offset, code))
408 }
409}
410
411fn parse(section: &[u8]) -> Option<TrapSection<'_>> {
412 let mut section = Bytes(section);
413 // NB: this matches the encoding written by `TrapEncodingBuilder`.
414 let entries = section.read::<U32<LittleEndian>>().ok()?;
415 let entries = usize::try_from(entries.get(LittleEndian)).ok()?;
416 let num_blocks = section.read::<U32<LittleEndian>>().ok()?;
417 let num_blocks = usize::try_from(num_blocks.get(LittleEndian)).ok()?;
418 let (block_index, block_bodies) =
419 object::slice_from_bytes::<[U32<LittleEndian>; 2]>(section.0, num_blocks).ok()?;
420 Some(TrapSection {
421 entries,
422 block_index,
423 block_bodies,
424 })
425}
426
427/// Returns an iterator over all of the traps encoded in `section`, which should
428/// have been produced by `TrapEncodingBuilder`.
429pub fn iterate_traps(section: &[u8]) -> Option<impl Iterator<Item = (u32, CompiledTrap)> + '_> {
430 let section = parse(section)?;
431 Some(
432 (0..section.block_index.len())
433 .flat_map(move |block| section.block_entries(block).into_iter().flatten())
434 .map(|(pc, byte)| (pc, CompiledTrap::from_u8(byte).unwrap())),
435 )
436}