Skip to content

Commit 8f64d50

Browse files
committed
Fix #38
1 parent 3833799 commit 8f64d50

File tree

5 files changed

+279
-1
lines changed

5 files changed

+279
-1
lines changed

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ widestring = { version = "1.2", features = ["std"], default-features = false }
2828

2929
[target.'cfg(not(windows))'.dependencies]
3030
cstr = { version = "0.2", default-features = false }
31+
libc = { version = "0.2", default-features = false, optional = true }
3132

3233
[dev-dependencies]
3334
trybuild = "1.0"
@@ -37,11 +38,15 @@ widestring = "1.2"
3738
rusty-fork = "0.3"
3839
path-absolutize = "3.1"
3940

41+
[target.'cfg(not(windows))'.dev-dependencies]
42+
libc = { version = "0.2", default-features = false }
43+
4044
[features]
41-
default = ["nethost-download", "net10_0"]
45+
default = ["nethost-download", "net10_0", "utils"]
4246
nethost-download = ["nethost", "nethost-sys/download-nuget"]
4347
nethost = ["nethost-sys"]
4448
nightly = []
49+
utils = ["libc"]
4550
doc-cfg = []
4651
netcore1_0 = ["hostfxr-sys/netcore1_0"]
4752
netcore2_0 = ["hostfxr-sys/netcore2_0", "netcore1_0"]

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,6 @@ pub mod error;
186186

187187
#[doc(hidden)]
188188
pub use hostfxr_sys::dlopen2;
189+
190+
/// Module containing additional utilities. (currently unix-only)
191+
pub mod utils;

src/utils.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#![cfg(unix)]
2+
3+
/// Utilities for configuring the alternate signal stack on Unix platforms.
4+
///
5+
/// Rust installs a small alternate signal stack for handling certain signals such as `SIGSEGV`.
6+
/// When embedding or hosting the .NET CoreCLR inside Rust, this small stack may be insufficient
7+
/// for the CLR exception-handling mechanisms. Increasing or disabling the alternate signal stack
8+
/// may be necessary to avoid a segfault in such cases.
9+
///
10+
/// See https://github.com/OpenByteDev/netcorehost/issues/38 for more details.
11+
pub mod altstack {
12+
use libc::{
13+
mmap, sigaltstack, stack_t, MAP_ANON, MAP_FAILED, MAP_PRIVATE, PROT_READ, PROT_WRITE,
14+
SS_DISABLE,
15+
};
16+
use std::{io, mem::MaybeUninit, ptr};
17+
18+
/// Represents the desired configuration of the alternate signal stack.
19+
pub enum State {
20+
/// Disables Rust's alternate signal stack.
21+
Disabled,
22+
23+
/// Enables and sets the alternate signal stack to a given size in bytes.
24+
Enabled {
25+
/// Target altstack size
26+
size: usize,
27+
},
28+
}
29+
30+
/// Configures the alternate signal stack according to the provided status.
31+
pub fn set(state: State) -> io::Result<()> {
32+
match state {
33+
State::Disabled => {
34+
let ss = stack_t {
35+
ss_flags: SS_DISABLE,
36+
ss_sp: ptr::null_mut(),
37+
ss_size: 0,
38+
};
39+
40+
let result = unsafe { sigaltstack(&ss, ptr::null_mut()) };
41+
if result != 0 {
42+
return Err(io::Error::last_os_error());
43+
}
44+
}
45+
46+
State::Enabled { size } => {
47+
let ptr = unsafe {
48+
mmap(
49+
ptr::null_mut(),
50+
size,
51+
PROT_READ | PROT_WRITE,
52+
MAP_PRIVATE | MAP_ANON,
53+
-1,
54+
0,
55+
)
56+
};
57+
58+
if ptr == MAP_FAILED {
59+
return Err(io::Error::last_os_error());
60+
}
61+
62+
let ss = stack_t {
63+
ss_sp: ptr,
64+
ss_size: size,
65+
ss_flags: 0,
66+
};
67+
68+
let result = unsafe { sigaltstack(&ss, ptr::null_mut()) };
69+
if result != 0 {
70+
return Err(io::Error::last_os_error());
71+
}
72+
}
73+
}
74+
75+
Ok(())
76+
}
77+
78+
/// Returns the current altstack status.
79+
pub fn get() -> io::Result<State> {
80+
let mut current = MaybeUninit::uninit();
81+
let result = unsafe { sigaltstack(ptr::null(), current.as_mut_ptr()) };
82+
if result != 0 {
83+
return Err(io::Error::last_os_error());
84+
}
85+
86+
let current = unsafe { current.assume_init() };
87+
let enabled = current.ss_flags & SS_DISABLE == 0;
88+
let state = if enabled {
89+
State::Enabled {
90+
size: current.ss_size,
91+
}
92+
} else {
93+
State::Disabled
94+
};
95+
96+
Ok(state)
97+
}
98+
}

tests/Test/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,12 @@ public static int UnmanagedHello() {
3030
}
3131

3232
public static int Main() => Hello(default, default);
33+
34+
class Foo { public int bar; }
35+
[UnmanagedCallersOnly]
36+
public static void Throw() {
37+
Foo foo = null!;
38+
foo.bar = 42;
39+
}
3340
}
3441
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#![cfg(all(feature = "netcore3_0", feature = "utils", unix))]
2+
// see https://github.com/OpenByteDev/netcorehost/issues/38
3+
4+
#[path = "common.rs"]
5+
mod common;
6+
7+
use netcorehost::{nethost, pdcstr, utils::altstack::{self, State}};
8+
use rusty_fork::{ChildWrapper, ExitStatusWrapper, fork, rusty_fork_id};
9+
use std::{process::Stdio, io::Read};
10+
11+
macro_rules! function_name {
12+
() => {{
13+
fn f() {}
14+
fn type_name_of<T>(_: T) -> &'static str {
15+
std::any::type_name::<T>()
16+
}
17+
type_name_of(f)
18+
.rsplit("::")
19+
.find(|&part| part != "f" && part != "{{closure}}")
20+
.expect("faled to get function name")
21+
}};
22+
}
23+
24+
macro_rules! assert_contains {
25+
($string:expr, $substring:expr $(,)?) => {{
26+
let string_ref: &str = &$string;
27+
let substring_ref: &str = &$substring;
28+
assert!(
29+
string_ref.contains(substring_ref),
30+
"Expected `{}` to contain `{}`",
31+
string_ref,
32+
substring_ref
33+
);
34+
}};
35+
}
36+
37+
macro_rules! assert_not_contains {
38+
($string:expr, $substring:expr $(,)?) => {{
39+
let string_ref: &str = &$string;
40+
let substring_ref: &str = &$substring;
41+
assert!(
42+
!string_ref.contains(substring_ref),
43+
"Expected `{}` NOT to contain `{}`",
44+
string_ref,
45+
substring_ref
46+
);
47+
}};
48+
}
49+
50+
const MANAGED_HANDLER_OUTPUT: &str = "Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.";
51+
52+
#[test]
53+
fn segfault_with_small_altstack() {
54+
common::setup();
55+
altstack_test(
56+
function_name!(),
57+
|| {
58+
altstack::set(State::Enabled { size: 2 * 1024 }).unwrap();
59+
},
60+
|status, _, stderr| {
61+
assert_eq!(status.unix_signal(), Some(libc::SIGSEGV));
62+
assert_not_contains!(stderr, MANAGED_HANDLER_OUTPUT);
63+
}
64+
);
65+
}
66+
67+
#[test]
68+
fn no_segfault_with_large_altstack() {
69+
common::setup();
70+
altstack_test(
71+
function_name!(),
72+
|| {
73+
altstack::set(State::Enabled { size: 16 * 1024 }).unwrap();
74+
},
75+
|status, _, stderr| {
76+
assert_ne!(status.unix_signal(), Some(libc::SIGSEGV));
77+
assert_contains!(stderr, MANAGED_HANDLER_OUTPUT);
78+
}
79+
);
80+
}
81+
82+
#[test]
83+
fn no_segfault_with_altstack_disabled() {
84+
common::setup();
85+
altstack_test(
86+
function_name!(),
87+
|| {
88+
altstack::set(State::Disabled).unwrap();
89+
},
90+
|status, _, stderr| {
91+
assert_ne!(status.unix_signal(), Some(libc::SIGSEGV));
92+
assert_contains!(stderr, MANAGED_HANDLER_OUTPUT);
93+
}
94+
);
95+
}
96+
97+
fn altstack_test(
98+
test_name: &str,
99+
configure_altstack: impl FnOnce(),
100+
verify: impl FnOnce(ExitStatusWrapper, /* stdout */ String, /* stderr */String)
101+
) {
102+
common::setup();
103+
104+
let body = || {
105+
configure_altstack();
106+
107+
let hostfxr = nethost::load_hostfxr().unwrap();
108+
109+
let context = hostfxr
110+
.initialize_for_runtime_config(common::test_runtime_config_path())
111+
.unwrap();
112+
let fn_loader = context
113+
.get_delegate_loader_for_assembly(common::test_dll_path())
114+
.unwrap();
115+
116+
let throw_fn = fn_loader
117+
.get_function_with_unmanaged_callers_only::<unsafe fn()>(
118+
pdcstr!("Test.Program, Test"),
119+
pdcstr!("Throw"),
120+
)
121+
.unwrap();
122+
unsafe { throw_fn() };
123+
};
124+
125+
fn configure_child(child: &mut std::process::Command) {
126+
child.stdout(Stdio::piped());
127+
child.stderr(Stdio::piped());
128+
}
129+
130+
// Define how to supervise the child process
131+
let supervise = |child: &mut ChildWrapper, _file: &mut std::fs::File| {
132+
let mut stdout = String::new();
133+
child
134+
.inner_mut()
135+
.stdout
136+
.as_mut()
137+
.unwrap()
138+
.read_to_string(&mut stdout)
139+
.unwrap();
140+
141+
let mut stderr = String::new();
142+
child
143+
.inner_mut()
144+
.stderr
145+
.as_mut()
146+
.unwrap()
147+
.read_to_string(&mut stderr)
148+
.unwrap();
149+
150+
let status = child.wait().expect("unable to wait for child");
151+
println!("status: {status}");
152+
println!("stdout: {stdout}");
153+
println!("stderr: {stderr}");
154+
verify(status, stdout, stderr);
155+
};
156+
157+
// Run the test in a forked child
158+
fork(
159+
test_name,
160+
rusty_fork_id!(),
161+
configure_child,
162+
supervise,
163+
body,
164+
).expect("fork failed");
165+
}

0 commit comments

Comments
 (0)