Skip to content

Commit 2690468

Browse files
committed
Auto merge of #92911 - nbdd0121:unwind, r=Amanieu
Guard against unwinding in cleanup code Currently the only safe guard we have against double unwind is the panic count (which is local to Rust). When double unwinds indeed happen (e.g. C++ exception + Rust panic, or two C++ exceptions), then the second unwind actually goes through and the first unwind is leaked. This can cause UB. cc rust-lang/project-ffi-unwind#6 E.g. given the following C++ code: ```c++ extern "C" void foo() { throw "A"; } extern "C" void execute(void (*fn)()) { try { fn(); } catch(...) { } } ``` This program is well-defined to terminate: ```c++ struct dtor { ~dtor() noexcept(false) { foo(); } }; void a() { dtor a; dtor b; } int main() { execute(a); return 0; } ``` But this Rust code doesn't catch the double unwind: ```rust extern "C-unwind" { fn foo(); fn execute(f: unsafe extern "C-unwind" fn()); } struct Dtor; impl Drop for Dtor { fn drop(&mut self) { unsafe { foo(); } } } extern "C-unwind" fn a() { let _a = Dtor; let _b = Dtor; } fn main() { unsafe { execute(a) }; } ``` To address this issue, this PR adds an unwind edge to an abort block, so that the Rust example aborts. This is similar to how clang guards against double unwind (except clang calls terminate per C++ spec and we abort). The cost should be very small; it's an additional trap instruction (well, two for now, since we use TrapUnreachable, but that's a different issue) for each function with landing pads; if LLVM gains support to encode "abort/terminate" info directly in LSDA like GCC does, then it'll be free. It's an additional basic block though so compile time may be worse, so I'd like a perf run. r? `@ghost` `@rustbot` label: F-c_unwind
2 parents 3b348d9 + 7d683f5 commit 2690468

File tree

8 files changed

+132
-27
lines changed

8 files changed

+132
-27
lines changed

compiler/rustc_codegen_ssa/src/mir/block.rs

+56-13
Original file line numberDiff line numberDiff line change
@@ -135,21 +135,38 @@ impl<'a, 'tcx> TerminatorCodegenHelper<'tcx> {
135135
// If there is a cleanup block and the function we're calling can unwind, then
136136
// do an invoke, otherwise do a call.
137137
let fn_ty = bx.fn_decl_backend_type(&fn_abi);
138-
if let Some(cleanup) = cleanup.filter(|_| fn_abi.can_unwind) {
138+
139+
let unwind_block = if let Some(cleanup) = cleanup.filter(|_| fn_abi.can_unwind) {
140+
Some(self.llblock(fx, cleanup))
141+
} else if fx.mir[self.bb].is_cleanup
142+
&& fn_abi.can_unwind
143+
&& !base::wants_msvc_seh(fx.cx.tcx().sess)
144+
{
145+
// Exception must not propagate out of the execution of a cleanup (doing so
146+
// can cause undefined behaviour). We insert a double unwind guard for
147+
// functions that can potentially unwind to protect against this.
148+
//
149+
// This is not necessary for SEH which does not use successive unwinding
150+
// like Itanium EH. EH frames in SEH are different from normal function
151+
// frames and SEH will abort automatically if an exception tries to
152+
// propagate out from cleanup.
153+
Some(fx.double_unwind_guard())
154+
} else {
155+
None
156+
};
157+
158+
if let Some(unwind_block) = unwind_block {
139159
let ret_llbb = if let Some((_, target)) = destination {
140160
fx.llbb(target)
141161
} else {
142162
fx.unreachable_block()
143163
};
144-
let invokeret = bx.invoke(
145-
fn_ty,
146-
fn_ptr,
147-
&llargs,
148-
ret_llbb,
149-
self.llblock(fx, cleanup),
150-
self.funclet(fx),
151-
);
164+
let invokeret =
165+
bx.invoke(fn_ty, fn_ptr, &llargs, ret_llbb, unwind_block, self.funclet(fx));
152166
bx.apply_attrs_callsite(&fn_abi, invokeret);
167+
if fx.mir[self.bb].is_cleanup {
168+
bx.apply_attrs_to_cleanup_callsite(invokeret);
169+
}
153170

154171
if let Some((ret_dest, target)) = destination {
155172
let mut ret_bx = fx.build_block(target);
@@ -486,17 +503,14 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
486503
let span = terminator.source_info.span;
487504
self.set_debug_loc(&mut bx, terminator.source_info);
488505

489-
// Get the location information.
490-
let location = self.get_caller_location(&mut bx, terminator.source_info).immediate();
491-
492506
// Obtain the panic entry point.
493507
let def_id = common::langcall(bx.tcx(), Some(span), "", LangItem::PanicNoUnwind);
494508
let instance = ty::Instance::mono(bx.tcx(), def_id);
495509
let fn_abi = bx.fn_abi_of_instance(instance, ty::List::empty());
496510
let llfn = bx.get_fn_addr(instance);
497511

498512
// Codegen the actual panic invoke/call.
499-
helper.do_call(self, &mut bx, fn_abi, llfn, &[location], None, None);
513+
helper.do_call(self, &mut bx, fn_abi, llfn, &[], None, None);
500514
}
501515

502516
/// Returns `true` if this is indeed a panic intrinsic and codegen is done.
@@ -1398,6 +1412,35 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
13981412
})
13991413
}
14001414

1415+
fn double_unwind_guard(&mut self) -> Bx::BasicBlock {
1416+
self.double_unwind_guard.unwrap_or_else(|| {
1417+
assert!(!base::wants_msvc_seh(self.cx.sess()));
1418+
1419+
let mut bx = self.new_block("abort");
1420+
self.set_debug_loc(&mut bx, mir::SourceInfo::outermost(self.mir.span));
1421+
1422+
let llpersonality = self.cx.eh_personality();
1423+
let llretty = self.landing_pad_type();
1424+
bx.cleanup_landing_pad(llretty, llpersonality);
1425+
1426+
let def_id = common::langcall(bx.tcx(), None, "", LangItem::PanicNoUnwind);
1427+
let instance = ty::Instance::mono(bx.tcx(), def_id);
1428+
let fn_abi = bx.fn_abi_of_instance(instance, ty::List::empty());
1429+
let fn_ptr = bx.get_fn_addr(instance);
1430+
let fn_ty = bx.fn_decl_backend_type(&fn_abi);
1431+
1432+
let llret = bx.call(fn_ty, fn_ptr, &[], None);
1433+
bx.apply_attrs_callsite(&fn_abi, llret);
1434+
bx.apply_attrs_to_cleanup_callsite(llret);
1435+
1436+
bx.unreachable();
1437+
let llbb = bx.llbb();
1438+
1439+
self.double_unwind_guard = Some(llbb);
1440+
llbb
1441+
})
1442+
}
1443+
14011444
// FIXME(eddyb) replace with `build_sibling_block`/`append_sibling_block`
14021445
// (which requires having a `Bx` already, and not all callers do).
14031446
fn new_block(&self, name: &str) -> Bx {

compiler/rustc_codegen_ssa/src/mir/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ pub struct FunctionCx<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> {
6262
/// Cached unreachable block
6363
unreachable_block: Option<Bx::BasicBlock>,
6464

65+
/// Cached double unwind guarding block
66+
double_unwind_guard: Option<Bx::BasicBlock>,
67+
6568
/// The location where each MIR arg/var/tmp/ret is stored. This is
6669
/// usually an `PlaceRef` representing an alloca, but not always:
6770
/// sometimes we can skip the alloca and just store the value
@@ -169,6 +172,7 @@ pub fn codegen_mir<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
169172
personality_slot: None,
170173
cached_llbbs,
171174
unreachable_block: None,
175+
double_unwind_guard: None,
172176
cleanup_kinds,
173177
landing_pads: IndexVec::from_elem(None, mir.basic_blocks()),
174178
funclets: IndexVec::from_fn_n(|_| None, mir.basic_blocks().len()),

library/core/src/panicking.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ fn panic_bounds_check(index: usize, len: usize) -> ! {
8787

8888
#[cfg(not(bootstrap))]
8989
#[cold]
90-
#[cfg_attr(not(feature = "panic_immediate_abort"), inline(never))]
91-
#[track_caller]
90+
#[inline(never)]
9291
#[lang = "panic_no_unwind"] // needed by codegen for panic in nounwind function
9392
fn panic_no_unwind() -> ! {
9493
if cfg!(feature = "panic_immediate_abort") {

src/test/codegen/drop.rs

+1-11
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,7 @@ pub fn droppy() {
2323
// FIXME(eddyb) the `void @` forces a match on the instruction, instead of the
2424
// comment, that's `; call core::ptr::drop_in_place::<drop::SomeUniqueName>`
2525
// for the `v0` mangling, should switch to matching on that once `legacy` is gone.
26-
// CHECK-NOT: invoke void @{{.*}}drop_in_place{{.*}}SomeUniqueName
27-
// CHECK: call void @{{.*}}drop_in_place{{.*}}SomeUniqueName
28-
// CHECK: call void @{{.*}}drop_in_place{{.*}}SomeUniqueName
29-
// CHECK-NOT: call void @{{.*}}drop_in_place{{.*}}SomeUniqueName
30-
// CHECK: invoke void @{{.*}}drop_in_place{{.*}}SomeUniqueName
31-
// CHECK-NOT: invoke void @{{.*}}drop_in_place{{.*}}SomeUniqueName
32-
// CHECK: call void @{{.*}}drop_in_place{{.*}}SomeUniqueName
33-
// CHECK-NOT: call void @{{.*}}drop_in_place{{.*}}SomeUniqueName
34-
// CHECK: invoke void @{{.*}}drop_in_place{{.*}}SomeUniqueName
35-
// CHECK-NOT: invoke void @{{.*}}drop_in_place{{.*}}SomeUniqueName
36-
// CHECK: call void @{{.*}}drop_in_place{{.*}}SomeUniqueName
26+
// CHECK-COUNT-6: {{(call|invoke) void @.*}}drop_in_place{{.*}}SomeUniqueName
3727
// CHECK-NOT: {{(call|invoke) void @.*}}drop_in_place{{.*}}SomeUniqueName
3828
// The next line checks for the } that ends the function definition
3929
// CHECK-LABEL: {{^[}]}}

src/test/codegen/unwind-landingpad-cold.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// get the `cold` attribute.
77

88
// CHECK-LABEL: @check_cold
9-
// CHECK: call void {{.+}}drop_in_place{{.+}} [[ATTRIBUTES:#[0-9]+]]
9+
// CHECK: {{(call|invoke) void .+}}drop_in_place{{.+}} [[ATTRIBUTES:#[0-9]+]]
1010
// CHECK: attributes [[ATTRIBUTES]] = { cold }
1111
#[no_mangle]
1212
pub fn check_cold(f: fn(), x: Box<u32>) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-include ../tools.mk
2+
3+
all: foo
4+
$(call RUN,foo) | $(CGREP) -v unreachable
5+
6+
foo: foo.rs $(call NATIVE_STATICLIB,foo)
7+
$(RUSTC) $< -lfoo $(EXTRARSCXXFLAGS)
8+
9+
$(TMPDIR)/libfoo.o: foo.cpp
10+
$(call COMPILE_OBJ_CXX,$@,$<)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include <cstdio>
2+
#include <exception>
3+
4+
void println(const char* s) {
5+
puts(s);
6+
fflush(stdout);
7+
}
8+
9+
struct outer_exception {};
10+
struct inner_exception {};
11+
12+
extern "C" {
13+
void throw_cxx_exception() {
14+
if (std::uncaught_exception()) {
15+
println("throwing inner C++ exception");
16+
throw inner_exception();
17+
} else {
18+
println("throwing outer C++ exception");
19+
throw outer_exception();
20+
}
21+
}
22+
23+
void cxx_catch_callback(void (*cb)()) {
24+
try {
25+
cb();
26+
println("unreachable: callback returns");
27+
} catch (outer_exception) {
28+
println("unreachable: caught outer exception in catch (...)");
29+
} catch (inner_exception) {
30+
println("unreachable: caught inner exception in catch (...)");
31+
}
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Tests that C++ double unwinding through Rust code will be properly guarded
2+
// against instead of exhibiting undefined behaviour.
3+
4+
#![feature(c_unwind)]
5+
6+
extern "C-unwind" {
7+
fn throw_cxx_exception();
8+
fn cxx_catch_callback(cb: extern "C-unwind" fn());
9+
}
10+
11+
struct ThrowOnDrop;
12+
13+
impl Drop for ThrowOnDrop {
14+
fn drop(&mut self) {
15+
unsafe { throw_cxx_exception() };
16+
}
17+
}
18+
19+
extern "C-unwind" fn test_double_unwind() {
20+
let _a = ThrowOnDrop;
21+
let _b = ThrowOnDrop;
22+
}
23+
24+
fn main() {
25+
unsafe { cxx_catch_callback(test_double_unwind) };
26+
}

0 commit comments

Comments
 (0)