Skip to content

Commit 8bf341a

Browse files
committed
Stow epoch-check code locations in a custom section of the native binary.
This will let the signal handler distinguish epoch interrupts from other segfaults. * Fix ISLE bug that was causing emit.rs code not to be triggered. * Bless inconsequential destination register reassignment in existing disas test.
1 parent 1f50af1 commit 8bf341a

11 files changed

Lines changed: 211 additions & 8 deletions

File tree

cranelift/codegen/src/isa/x64/inst/emit.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,7 @@ pub(crate) fn emit(
649649
// The ISLE has already emitted the dead load. Put the address of
650650
// this instruction aside so we can later distinguish whether a
651651
// segfault is its fault.
652-
653-
// Search for "let pc_offset = layout.ip_offset as i32;" as a string to pull on.
652+
sink.add_epoch_check();
654653
}
655654

656655
Inst::JmpKnown { dst } => uncond_jmp(sink, *dst),

cranelift/codegen/src/isa/x64/lower.isle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3592,7 +3592,7 @@
35923592
load_ptr
35933593
(zero_offset))
35943594
(ExtKind.None)))
3595-
(_ SideEffectNoResult (x64_dead_load_with_context load_ptr context)))
3595+
(_ Unit (emit_side_effect (x64_dead_load_with_context load_ptr context))))
35963596
(output_none)))
35973597

35983598
;;;; Rules for `get_{frame,stack}_pointer` and `get_return_address` ;;;;;;;;;;;;

cranelift/codegen/src/machinst/buffer.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ pub struct MachBuffer<I: VCodeInst> {
252252
call_sites: SmallVec<[MachCallSite; 16]>,
253253
/// Any patchable call site locations.
254254
patchable_call_sites: SmallVec<[MachPatchableCallSite; 16]>,
255+
/// Any locations which do an MMU-based check for the end of an epoch.
256+
epoch_checks: SmallVec<[EpochCheckOffset; 16]>,
255257
/// Any exception-handler records referred to at call sites.
256258
exception_handlers: SmallVec<[MachExceptionHandler; 16]>,
257259
/// Any source location mappings referring to this code.
@@ -343,6 +345,7 @@ impl MachBufferFinalized<Stencil> {
343345
traps: self.traps,
344346
call_sites: self.call_sites,
345347
patchable_call_sites: self.patchable_call_sites,
348+
epoch_checks: self.epoch_checks,
346349
exception_handlers: self.exception_handlers,
347350
srclocs: self
348351
.srclocs
@@ -380,6 +383,8 @@ pub struct MachBufferFinalized<T: CompilePhase> {
380383
pub(crate) call_sites: SmallVec<[MachCallSite; 16]>,
381384
/// Any patchable call site locations refering to this code.
382385
pub(crate) patchable_call_sites: SmallVec<[MachPatchableCallSite; 16]>,
386+
/// Any locations which do an MMU-based check for the end of an epoch.
387+
pub epoch_checks: SmallVec<[EpochCheckOffset; 16]>,
383388
/// Any exception-handler records referred to at call sites.
384389
pub(crate) exception_handlers: SmallVec<[FinalizedMachExceptionHandler; 16]>,
385390
/// Any source location mappings referring to this code.
@@ -480,6 +485,7 @@ impl<I: VCodeInst> MachBuffer<I> {
480485
traps: SmallVec::new(),
481486
call_sites: SmallVec::new(),
482487
patchable_call_sites: SmallVec::new(),
488+
epoch_checks: SmallVec::new(),
483489
exception_handlers: SmallVec::new(),
484490
srclocs: SmallVec::new(),
485491
debug_tags: vec![],
@@ -1581,6 +1587,7 @@ impl<I: VCodeInst> MachBuffer<I> {
15811587
traps: self.traps,
15821588
call_sites: self.call_sites,
15831589
patchable_call_sites: self.patchable_call_sites,
1590+
epoch_checks: self.epoch_checks,
15841591
exception_handlers: finalized_exception_handlers,
15851592
srclocs,
15861593
debug_tags: self.debug_tags,
@@ -1696,6 +1703,14 @@ impl<I: VCodeInst> MachBuffer<I> {
16961703
});
16971704
}
16981705

1706+
/// Record that an MMU-based epoch interruption check occurs at the current
1707+
/// offset. The signal handler uses these annotations to distinguish that a
1708+
/// segfault is actually an epoch interruption in disguise. The
1709+
/// DeadLoadWithContext instruction is assumed to have already been emitted.
1710+
pub fn add_epoch_check(&mut self) {
1711+
self.epoch_checks.push(self.cur_offset());
1712+
}
1713+
16991714
/// Add an unwind record at the current offset.
17001715
pub fn add_unwind(&mut self, unwind: UnwindInst) {
17011716
self.unwind_info.push((self.cur_offset(), unwind));
@@ -2198,6 +2213,12 @@ pub struct MachPatchableCallSite {
21982213
pub len: u32,
21992214
}
22002215

2216+
/// The location of an epoch-end check, when using MMU-based epoch interruption.
2217+
///
2218+
/// Specifically, this points to the instruction after the one that does the
2219+
/// epoch-end check: the one at which to resume execution.
2220+
pub type EpochCheckOffset = CodeOffset;
2221+
22012222
/// A source-location mapping resulting from a compilation.
22022223
#[derive(PartialEq, Debug, Clone)]
22032224
#[cfg_attr(

crates/cranelift/src/compiler.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ use wasmparser::{FuncValidatorAllocations, FunctionBody};
3535
use wasmtime_environ::obj::{ELF_WASMTIME_EXCEPTIONS, ELF_WASMTIME_FRAMES};
3636
use wasmtime_environ::{
3737
Abi, AddressMapSection, BuiltinFunctionIndex, CacheStore, CompileError, CompiledFunctionBody,
38-
DefinedFuncIndex, FlagValue, FrameInstPos, FrameStackShape, FrameStateSlotBuilder,
39-
FrameTableBuilder, FuncKey, FunctionBodyData, FunctionLoc, HostCall, InliningCompiler,
40-
ModuleTranslation, ModuleTypesBuilder, PtrSize, StackMapSection, StaticModuleIndex,
41-
TrapEncodingBuilder, TrapSentinel, TripleExt, Tunables, WasmFuncType, WasmValType,
38+
DefinedFuncIndex, EpochCheckSection, FlagValue, FrameInstPos, FrameStackShape,
39+
FrameStateSlotBuilder, FrameTableBuilder, FuncKey, FunctionBodyData, FunctionLoc, HostCall,
40+
InliningCompiler, ModuleTranslation, ModuleTypesBuilder, PtrSize, StackMapSection,
41+
StaticModuleIndex, TrapEncodingBuilder, TrapSentinel, TripleExt, Tunables, WasmFuncType,
42+
WasmValType,
4243
};
4344
use wasmtime_unwinder::ExceptionTableBuilder;
4445

@@ -468,6 +469,7 @@ impl wasmtime_environ::Compiler for Compiler {
468469
let mut stack_maps = StackMapSection::default();
469470
let mut exception_tables = ExceptionTableBuilder::default();
470471
let mut frame_tables = FrameTableBuilder::default();
472+
let mut epoch_checks = EpochCheckSection::default();
471473

472474
let funcs = funcs
473475
.iter()
@@ -536,6 +538,9 @@ impl wasmtime_environ::Compiler for Compiler {
536538
)?;
537539
nop_units.get_or_insert_with(|| func.buffer.nop_units.clone());
538540
}
541+
if self.tunables.epoch_interruption_via_mmu {
542+
epoch_checks.push(range.clone(), &func.buffer.epoch_checks);
543+
}
539544
builder.append_padding(self.linkopts.padding_between_functions);
540545

541546
let info = FunctionLoc {
@@ -582,6 +587,9 @@ impl wasmtime_environ::Compiler for Compiler {
582587
}
583588
stack_maps.append_to(obj);
584589
traps.append_to(obj);
590+
if self.tunables.epoch_interruption_via_mmu {
591+
epoch_checks.append_to(obj);
592+
}
585593

586594
let exception_section = obj.add_section(
587595
obj.segment_name(StandardSegment::Data).to_vec(),
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Emission of compiled-artifact metadata describing where epoch-end checks
2+
//! occur in the code when using MMU-based epoch interruption. This lets the
3+
//! signal handler distinguish epoch interruptions from general segfaults.
4+
5+
use crate::obj::ELF_WASMTIME_EPOCH_CHECKS;
6+
use crate::prelude::*;
7+
use object::write::{Object, StandardSegment};
8+
use object::{LittleEndian, SectionKind, U32Bytes};
9+
use std::ops::Range;
10+
11+
/// Offset of an epoch check within its function, in bytes. Specifically, this
12+
/// points to The instruction after the one that does the epoch-end check: the
13+
/// one at which to resume execution.
14+
///
15+
/// This is parallel to cranelift's CodeOffset and exists to (1) avoid making it
16+
/// a dependency, (2) pin it down to <= 32 bits, since the format of the
17+
/// custom-section we're emitting depends on that, and (3) hold documentation.
18+
type EpochCheckOffset = u32;
19+
20+
/// A builder and emitter of the custom section which houses MMU-based
21+
/// epoch-end-check locations in a native binary.
22+
#[derive(Default)]
23+
pub struct EpochCheckSection {
24+
/// Offset of the instruction to resume at after the epoch end and task switch
25+
return_offsets: Vec<U32Bytes<LittleEndian>>,
26+
/// The largest (and most recent, because we accept them only in order)
27+
/// offset received so far for enforcing ordering. This is relative to the
28+
/// start of the code (text) section so we can make sure functions are
29+
/// ordered too.
30+
last_offset: u32,
31+
}
32+
33+
impl EpochCheckSection {
34+
/// Adds an epoch-check location to the section.
35+
///
36+
/// Calls to this must be ordered by the location of `func`, and
37+
/// `check_offsets` must be ordered (within each function) as well.
38+
pub fn push(&mut self, func: Range<u64>, check_offsets: &[EpochCheckOffset]) {
39+
// Check that functions have been pushed in order so our section is
40+
// sorted for free.
41+
let func_start = u32::try_from(func.start).unwrap();
42+
let func_end = u32::try_from(func.end).unwrap();
43+
assert!(func_start >= self.last_offset);
44+
45+
// Remember each offset, ensuring they are in order.
46+
for offset in check_offsets {
47+
let text_section_relative = func_start + offset;
48+
assert!(text_section_relative > self.last_offset);
49+
self.return_offsets
50+
.push(U32Bytes::new(LittleEndian, text_section_relative));
51+
self.last_offset = text_section_relative;
52+
}
53+
self.last_offset = func_end;
54+
}
55+
56+
/// Encodes this section into an object.
57+
pub fn append_to(self, obj: &mut Object) {
58+
let section = obj.add_section(
59+
obj.segment_name(StandardSegment::Data).to_vec(),
60+
ELF_WASMTIME_EPOCH_CHECKS.as_bytes().to_vec(),
61+
SectionKind::ReadOnlyData,
62+
);
63+
64+
// Append length.
65+
obj.append_section_data(
66+
section,
67+
&u32::try_from(self.return_offsets.len())
68+
.unwrap()
69+
.to_le_bytes(),
70+
1,
71+
);
72+
73+
// Append offsets.
74+
obj.append_section_data(section, object::bytes_of_slice(&self.return_offsets), 1);
75+
}
76+
}

crates/environ/src/compile/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::path;
1616
use std::sync::Arc;
1717

1818
mod address_map;
19+
mod epoch_checks;
1920
mod frame_table;
2021
mod module_artifacts;
2122
mod module_environ;
@@ -24,6 +25,7 @@ mod stack_maps;
2425
mod trap_encoding;
2526

2627
pub use self::address_map::*;
28+
pub use self::epoch_checks::*;
2729
pub use self::frame_table::*;
2830
pub use self::module_artifacts::*;
2931
pub use self::module_environ::*;

crates/environ/src/obj.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ pub const ELF_WASMTIME_STACK_MAP: &str = ".wasmtime.stackmap";
9898
/// to the 32-bit encodings for offsets this doesn't support images >=4gb.
9999
pub const ELF_WASMTIME_TRAPS: &str = ".wasmtime.traps";
100100

101+
/// A custom section which contains the offsets of instructions which check for
102+
/// the end of epochs when using `--epoch-interruption-via-mmu`.
103+
///
104+
/// The contents are examined at runtime by the signal handler to determine
105+
/// whether a segfault is due to an epoch ending (vs. a legitimate crash).
106+
///
107+
/// This section has a custom binary encoding:
108+
///
109+
/// * The section starts with a 32-bit little-endian integer which tell the
110+
/// number of items in the following array.
111+
/// * Next comes a sorted array of 32-bit unsigned little-endian integers which
112+
/// represent offsets from the beginning of the text section to the
113+
/// instruction following the load which triggers epoch-ending segfaults.
114+
/// TODO: We may point elsewhere if it's more useful to the signal handler. Be
115+
/// careful if this could end up pointing off the end of the text section.
116+
///
117+
/// The 32-bit encodings herein mean that >=4gb text sections are not supported.
118+
pub const ELF_WASMTIME_EPOCH_CHECKS: &str = ".wasmtime.epochchecks";
119+
101120
/// A custom binary-encoded section of the wasmtime compilation
102121
/// artifacts which encodes exception tables.
103122
///

tests/all/epoch_mmu.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#![cfg(not(miri))]
2+
3+
use object::{Object, ObjectSection};
4+
use wasmtime::{Config, Engine};
5+
use wasmtime_environ::obj::ELF_WASMTIME_EPOCH_CHECKS;
6+
7+
/// Asserts that each epoch-check offset encoded into the binary points to the
8+
/// byte after its corresponding dead load.
9+
#[test]
10+
fn epoch_check_offsets() {
11+
let mut config = Config::new();
12+
config.target("x86_64").unwrap();
13+
config.epoch_interruption_via_mmu(true);
14+
let engine = Engine::new(&config).unwrap();
15+
16+
// A function with an infinite loop contains two epoch checks: one in the
17+
// function prologue and another at the loop backedge.
18+
let elf_bytes = engine
19+
.precompile_module(
20+
// If you change this wat, change it in
21+
// epoch-interruption-mmu-compile-loop.wat, too.
22+
r#"(module
23+
(memory 0)
24+
(func (loop (br 0)))
25+
)"#
26+
.as_bytes(),
27+
)
28+
.unwrap();
29+
30+
let elf = object::read::elf::ElfFile64::<object::Endianness>::parse(&*elf_bytes)
31+
.expect("ELF should be parseable");
32+
let section = elf
33+
.section_by_name(ELF_WASMTIME_EPOCH_CHECKS)
34+
.expect(&format!(
35+
"{ELF_WASMTIME_EPOCH_CHECKS} section should be present"
36+
));
37+
let data = section.data().unwrap();
38+
assert!(data.len() >= 4, "section should at least contain a count");
39+
let count = u32::from_le_bytes(data[..4].try_into().unwrap()) as usize;
40+
assert_eq!(
41+
data.len(),
42+
4 + count * 4,
43+
"section should be the right size to hold the offsets it claims to contain"
44+
);
45+
let offsets: Vec<u32> = data[4..]
46+
.chunks_exact(4)
47+
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
48+
.collect();
49+
50+
// The emitted machine code is nailed down by the
51+
// epoch-interruption-mmu-compile-loop.wat disas test. As long as that keeps
52+
// passing, these offsets remain valid.
53+
assert_eq!(
54+
offsets,
55+
vec![15, 18],
56+
"There should be 2 epoch checks (function prologue & loop backedge). The offset after the prologue's dead load should be 15, and the one after the loop's backedge should be 18."
57+
);
58+
}

tests/all/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod debug;
1515
mod defaults;
1616
mod engine;
1717
mod epoch_interruption;
18+
mod epoch_mmu;
1819
mod eqref;
1920
mod exceptions;
2021
mod exnrefs;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
;;! target = "x86_64"
2+
;;! test = "compile"
3+
;;! flags = ["-Wepoch-interruption-via-mmu=y"]
4+
5+
;; Nail down codegen for the snippet in epoch_check_offsets() test. If this
6+
;; starts failing, that may need the offsets in its assert reexamined.
7+
8+
(module
9+
(memory 0)
10+
(func (loop (br 0)))
11+
)
12+
;; wasm[0]::function[0]:
13+
;; pushq %rbp
14+
;; movq %rsp, %rbp
15+
;; movq 8(%rdi), %r8
16+
;; movq 0x10(%r8), %r8
17+
;; movq (%r8), %r9
18+
;; movq (%r8), %r10
19+
;; jmp 0xf

0 commit comments

Comments
 (0)