Skip to content

Commit 2778e19

Browse files
committed
Add a support to execute isolated op without halting
In user interface, added a new flag `-Zmiri-isolation-error` which takes one of the four values -- hide, warn, warn-nobacktrace, and abort. This option can be used to configure Miri to either abort or return an error code upon executing isolated op. If not aborted, Miri prints a warning, whose verbosity can be configured using this flag. In implementation, added a new enum `IsolatedOp` to capture all the settings related to ops requiring communication with the host. Old `communicate` flag in both miri configs and machine stats is replaced with a new helper function `communicate()` which checks `isolated_op` internally. Added a new helper function `reject_in_isolation` which can be called by shims to reject ops according to the reject_with settings. Use miri specific diagnostics function `report_msg` to print backtrace in the warning. Update it to take an enum value instead of a bool, indicating the level of diagnostics. Updated shims related to current dir to use the new APIs. Added a new test for current dir ops in isolation without halting machine.
1 parent 28717a0 commit 2778e19

11 files changed

+199
-43
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,13 @@ environment variable:
219219
* `-Zmiri-disable-isolation` disables host isolation. As a consequence,
220220
the program has access to host resources such as environment variables, file
221221
systems, and randomness.
222+
* `-Zmiri-isolation-error=<action>` configures Miri's response to operations
223+
requiring host access while isolation is enabled. `abort`, `hide`, `warn`,
224+
and `warn-nobacktrace` are the supported actions. Default action is `abort`
225+
which halts the machine. Rest of the actions configure it to return an error
226+
code for the op and continue executing. `warn` prints backtrace that could
227+
be used to trace the call. `warn-nobacktrace` is less verbose without
228+
backtrace. `hide` hides the warning.
222229
* `-Zmiri-env-exclude=<var>` keeps the `var` environment variable isolated from
223230
the host so that it cannot be accessed by the program. Can be used multiple
224231
times to exclude several variables. On Windows, the `TERM` environment

src/bin/miri.rs

+34-1
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ fn main() {
263263
let mut miri_config = miri::MiriConfig::default();
264264
let mut rustc_args = vec![];
265265
let mut after_dashdash = false;
266+
267+
// If user has explicitly enabled/disabled isolation
268+
let mut isolation_enabled: Option<bool> = None;
266269
for arg in env::args() {
267270
if rustc_args.is_empty() {
268271
// Very first arg: binary name.
@@ -291,7 +294,37 @@ fn main() {
291294
miri_config.check_abi = false;
292295
}
293296
"-Zmiri-disable-isolation" => {
294-
miri_config.communicate = true;
297+
if matches!(isolation_enabled, Some(true)) {
298+
panic!(
299+
"-Zmiri-disable-isolation cannot be used along with -Zmiri-isolation-error"
300+
);
301+
} else {
302+
isolation_enabled = Some(false);
303+
}
304+
miri_config.isolated_op = miri::IsolatedOp::Allow;
305+
}
306+
arg if arg.starts_with("-Zmiri-isolation-error=") => {
307+
if matches!(isolation_enabled, Some(false)) {
308+
panic!(
309+
"-Zmiri-isolation-error cannot be used along with -Zmiri-disable-isolation"
310+
);
311+
} else {
312+
isolation_enabled = Some(true);
313+
}
314+
315+
miri_config.isolated_op = match arg
316+
.strip_prefix("-Zmiri-isolation-error=")
317+
.unwrap()
318+
{
319+
"abort" => miri::IsolatedOp::Reject(miri::RejectOpWith::Abort),
320+
"hide" => miri::IsolatedOp::Reject(miri::RejectOpWith::NoWarning),
321+
"warn" => miri::IsolatedOp::Reject(miri::RejectOpWith::Warning),
322+
"warn-nobacktrace" =>
323+
miri::IsolatedOp::Reject(miri::RejectOpWith::WarningWithoutBacktrace),
324+
_ => panic!(
325+
"-Zmiri-isolation-error must be `abort`, `hide`, `warn`, or `warn-nobacktrace`"
326+
),
327+
};
295328
}
296329
"-Zmiri-ignore-leaks" => {
297330
miri_config.ignore_leaks = true;

src/diagnostics.rs

+26-15
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ pub enum NonHaltingDiagnostic {
5252
CreatedCallId(CallId),
5353
CreatedAlloc(AllocId),
5454
FreedAlloc(AllocId),
55+
RejectedIsolatedOp(String),
56+
}
57+
58+
/// Level of Miri specific diagnostics
59+
enum DiagLevel {
60+
Error,
61+
Warning,
62+
Note,
5563
}
5664

5765
/// Emit a custom diagnostic without going through the miri-engine machinery
@@ -76,7 +84,7 @@ pub fn report_error<'tcx, 'mir>(
7684
#[rustfmt::skip]
7785
let helps = match info {
7886
UnsupportedInIsolation(_) =>
79-
vec![(None, format!("pass the flag `-Zmiri-disable-isolation` to disable isolation"))],
87+
vec![(None, format!("pass the flag `-Zmiri-disable-isolation` to disable isolation; or pass `-Zmiri-isolation-error=warn to configure Miri to return an error code from isolated operations and continue with a warning"))],
8088
ExperimentalUb { url, .. } =>
8189
vec![
8290
(None, format!("this indicates a potential bug in the program: it performed an invalid operation, but the rules it violated are still experimental")),
@@ -137,7 +145,7 @@ pub fn report_error<'tcx, 'mir>(
137145
let msg = e.to_string();
138146
report_msg(
139147
*ecx.tcx,
140-
/*error*/ true,
148+
DiagLevel::Error,
141149
&if let Some(title) = title { format!("{}: {}", title, msg) } else { msg.clone() },
142150
msg,
143151
helps,
@@ -174,18 +182,19 @@ pub fn report_error<'tcx, 'mir>(
174182
/// Also emits a full stacktrace of the interpreter stack.
175183
fn report_msg<'tcx>(
176184
tcx: TyCtxt<'tcx>,
177-
error: bool,
185+
diag_level: DiagLevel,
178186
title: &str,
179187
span_msg: String,
180188
mut helps: Vec<(Option<SpanData>, String)>,
181189
stacktrace: &[FrameInfo<'tcx>],
182190
) {
183191
let span = stacktrace.first().map_or(DUMMY_SP, |fi| fi.span);
184-
let mut err = if error {
185-
tcx.sess.struct_span_err(span, title)
186-
} else {
187-
tcx.sess.diagnostic().span_note_diag(span, title)
192+
let mut err = match diag_level {
193+
DiagLevel::Error => tcx.sess.struct_span_err(span, title),
194+
DiagLevel::Warning => tcx.sess.struct_span_warn(span, title),
195+
DiagLevel::Note => tcx.sess.diagnostic().span_note_diag(span, title),
188196
};
197+
189198
// Show main message.
190199
if span != DUMMY_SP {
191200
err.span_label(span, span_msg);
@@ -303,15 +312,17 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
303312
CreatedCallId(id) => format!("function call with id {}", id),
304313
CreatedAlloc(AllocId(id)) => format!("created allocation with id {}", id),
305314
FreedAlloc(AllocId(id)) => format!("freed allocation with id {}", id),
315+
RejectedIsolatedOp(ref op) =>
316+
format!("`{}` was made to return an error due to isolation", op),
306317
};
307-
report_msg(
308-
*this.tcx,
309-
/*error*/ false,
310-
"tracking was triggered",
311-
msg,
312-
vec![],
313-
&stacktrace,
314-
);
318+
319+
let (title, diag_level) = match e {
320+
RejectedIsolatedOp(_) =>
321+
("operation rejected by isolation", DiagLevel::Warning),
322+
_ => ("tracking was triggered", DiagLevel::Note),
323+
};
324+
325+
report_msg(*this.tcx, diag_level, title, msg, vec![], &stacktrace);
315326
}
316327
});
317328
}

src/eval.rs

+33-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,35 @@ pub enum AlignmentCheck {
2222
Int,
2323
}
2424

25+
#[derive(Copy, Clone, Debug, PartialEq)]
26+
pub enum RejectOpWith {
27+
/// Isolated op is rejected with an abort of the machine.
28+
Abort,
29+
30+
/// If not Abort, miri returns an error for an isolated op.
31+
/// Following options determine if user should be warned about such error.
32+
/// Do not print warning about rejected isolated op.
33+
NoWarning,
34+
35+
/// Print a warning about rejected isolated op, with backtrace.
36+
Warning,
37+
38+
/// Print a warning about rejected isolated op, without backtrace.
39+
WarningWithoutBacktrace,
40+
}
41+
42+
#[derive(Copy, Clone, Debug, PartialEq)]
43+
pub enum IsolatedOp {
44+
/// Reject an op requiring communication with the host. By
45+
/// default, miri rejects the op with an abort. If not, it returns
46+
/// an error code, and prints a warning about it. Warning levels
47+
/// are controlled by `RejectOpWith` enum.
48+
Reject(RejectOpWith),
49+
50+
/// Execute op requiring communication with the host, i.e. disable isolation.
51+
Allow,
52+
}
53+
2554
/// Configuration needed to spawn a Miri instance.
2655
#[derive(Clone)]
2756
pub struct MiriConfig {
@@ -33,8 +62,8 @@ pub struct MiriConfig {
3362
pub check_alignment: AlignmentCheck,
3463
/// Controls function [ABI](Abi) checking.
3564
pub check_abi: bool,
36-
/// Determines if communication with the host environment is enabled.
37-
pub communicate: bool,
65+
/// Action for an op requiring communication with the host.
66+
pub isolated_op: IsolatedOp,
3867
/// Determines if memory leaks should be ignored.
3968
pub ignore_leaks: bool,
4069
/// Environment variables that should always be isolated from the host.
@@ -68,7 +97,7 @@ impl Default for MiriConfig {
6897
stacked_borrows: true,
6998
check_alignment: AlignmentCheck::Int,
7099
check_abi: true,
71-
communicate: false,
100+
isolated_op: IsolatedOp::Reject(RejectOpWith::Abort),
72101
ignore_leaks: false,
73102
excluded_env_vars: vec![],
74103
args: vec![],
@@ -233,7 +262,7 @@ pub fn eval_main<'tcx>(tcx: TyCtxt<'tcx>, main_id: DefId, config: MiriConfig) ->
233262
}
234263
SchedulingAction::ExecuteTimeoutCallback => {
235264
assert!(
236-
ecx.machine.communicate,
265+
ecx.machine.communicate(),
237266
"scheduler callbacks require disabled isolation, but the code \
238267
that created the callback did not check it"
239268
);

src/helpers.rs

+24-4
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
140140

141141
let mut data = vec![0; usize::try_from(len).unwrap()];
142142

143-
if this.machine.communicate {
143+
if this.machine.communicate() {
144144
// Fill the buffer using the host's rng.
145145
getrandom::getrandom(&mut data)
146146
.map_err(|err| err_unsup_format!("host getrandom failed: {}", err))?;
@@ -391,12 +391,32 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
391391
/// disabled. It returns an error using the `name` of the foreign function if this is not the
392392
/// case.
393393
fn check_no_isolation(&self, name: &str) -> InterpResult<'tcx> {
394-
if !self.eval_context_ref().machine.communicate {
395-
isolation_error(name)?;
394+
if !self.eval_context_ref().machine.communicate() {
395+
self.reject_in_isolation(name, RejectOpWith::Abort)?;
396396
}
397397
Ok(())
398398
}
399399

400+
/// Helper function used inside the shims of foreign functions which reject the op
401+
/// when isolation is enabled. It is used to print a warning/backtrace about the rejection.
402+
fn reject_in_isolation(&self, op_name: &str, reject_with: RejectOpWith) -> InterpResult<'tcx> {
403+
let this = self.eval_context_ref();
404+
match reject_with {
405+
RejectOpWith::Abort => isolation_abort_error(op_name),
406+
RejectOpWith::WarningWithoutBacktrace => {
407+
this.tcx
408+
.sess
409+
.warn(&format!("`{}` was made to return an error due to isolation", op_name));
410+
Ok(())
411+
}
412+
RejectOpWith::Warning => {
413+
register_diagnostic(NonHaltingDiagnostic::RejectedIsolatedOp(op_name.to_string()));
414+
Ok(())
415+
}
416+
RejectOpWith::NoWarning => Ok(()), // no warning
417+
}
418+
}
419+
400420
/// Helper function used inside the shims of foreign functions to assert that the target OS
401421
/// is `target_os`. It panics showing a message with the `name` of the foreign function
402422
/// if this is not the case.
@@ -643,7 +663,7 @@ where
643663
throw_ub_format!("incorrect number of arguments: got {}, expected {}", args.len(), N)
644664
}
645665

646-
pub fn isolation_error(name: &str) -> InterpResult<'static> {
666+
pub fn isolation_abort_error(name: &str) -> InterpResult<'static> {
647667
throw_machine_stop!(TerminationInfo::UnsupportedInIsolation(format!(
648668
"{} not available when isolation is enabled",
649669
name,

src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ pub use crate::diagnostics::{
5959
register_diagnostic, report_error, EvalContextExt as DiagnosticsEvalContextExt,
6060
NonHaltingDiagnostic, TerminationInfo,
6161
};
62-
pub use crate::eval::{create_ecx, eval_main, AlignmentCheck, MiriConfig};
62+
pub use crate::eval::{
63+
create_ecx, eval_main, AlignmentCheck, IsolatedOp, MiriConfig, RejectOpWith,
64+
};
6365
pub use crate::helpers::EvalContextExt as HelpersEvalContextExt;
6466
pub use crate::machine::{
6567
AllocExtra, Evaluator, FrameData, MemoryExtra, MiriEvalContext, MiriEvalContextExt,

src/machine.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,10 @@ pub struct Evaluator<'mir, 'tcx> {
263263
/// TLS state.
264264
pub(crate) tls: TlsData<'tcx>,
265265

266-
/// If enabled, the `env_vars` field is populated with the host env vars during initialization
267-
/// and random number generation is delegated to the host.
268-
pub(crate) communicate: bool,
266+
/// What should Miri do when an op requires communicating with the host,
267+
/// such as accessing host env vars, random number generation, and
268+
/// file system access.
269+
pub(crate) isolated_op: IsolatedOp,
269270

270271
/// Whether to enforce the validity invariant.
271272
pub(crate) validate: bool,
@@ -314,7 +315,7 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
314315
argv: None,
315316
cmd_line: None,
316317
tls: TlsData::default(),
317-
communicate: config.communicate,
318+
isolated_op: config.isolated_op,
318319
validate: config.validate,
319320
enforce_abi: config.check_abi,
320321
file_handler: Default::default(),
@@ -328,6 +329,10 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
328329
exported_symbols_cache: FxHashMap::default(),
329330
}
330331
}
332+
333+
pub(crate) fn communicate(&self) -> bool {
334+
self.isolated_op == IsolatedOp::Allow
335+
}
331336
}
332337

333338
/// A rustc InterpCx for Miri.

src/shims/env.rs

+29-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::convert::TryFrom;
22
use std::env;
33
use std::ffi::{OsStr, OsString};
4+
use std::io::{Error, ErrorKind};
45

56
use rustc_data_structures::fx::FxHashMap;
67
use rustc_mir::interpret::Pointer;
@@ -45,7 +46,7 @@ impl<'tcx> EnvVars<'tcx> {
4546
excluded_env_vars.push("TERM".to_owned());
4647
}
4748

48-
if ecx.machine.communicate {
49+
if ecx.machine.communicate() {
4950
for (name, value) in env::vars() {
5051
if !excluded_env_vars.contains(&name) {
5152
let var_ptr = match target_os {
@@ -321,7 +322,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
321322
"`getcwd` is only available for the UNIX target family"
322323
);
323324

324-
this.check_no_isolation("`getcwd`")?;
325+
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
326+
this.reject_in_isolation("getcwd", reject_with)?;
327+
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
328+
this.set_last_error_from_io_error(err)?;
329+
return Ok(Scalar::null_ptr(&*this.tcx));
330+
}
325331

326332
let buf = this.read_scalar(&buf_op)?.check_init()?;
327333
let size = this.read_scalar(&size_op)?.to_machine_usize(&*this.tcx)?;
@@ -336,6 +342,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
336342
}
337343
Err(e) => this.set_last_error_from_io_error(e)?,
338344
}
345+
339346
Ok(Scalar::null_ptr(&*this.tcx))
340347
}
341348

@@ -348,7 +355,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
348355
let this = self.eval_context_mut();
349356
this.assert_target_os("windows", "GetCurrentDirectoryW");
350357

351-
this.check_no_isolation("`GetCurrentDirectoryW`")?;
358+
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
359+
this.reject_in_isolation("GetCurrentDirectoryW", reject_with)?;
360+
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
361+
this.set_last_error_from_io_error(err)?;
362+
return Ok(0);
363+
}
352364

353365
let size = u64::from(this.read_scalar(size_op)?.to_u32()?);
354366
let buf = this.read_scalar(buf_op)?.check_init()?;
@@ -370,7 +382,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
370382
"`getcwd` is only available for the UNIX target family"
371383
);
372384

373-
this.check_no_isolation("`chdir`")?;
385+
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
386+
this.reject_in_isolation("chdir", reject_with)?;
387+
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
388+
this.set_last_error_from_io_error(err)?;
389+
390+
return Ok(-1);
391+
}
374392

375393
let path = this.read_path_from_c_str(this.read_scalar(path_op)?.check_init()?)?;
376394

@@ -393,7 +411,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
393411
let this = self.eval_context_mut();
394412
this.assert_target_os("windows", "SetCurrentDirectoryW");
395413

396-
this.check_no_isolation("`SetCurrentDirectoryW`")?;
414+
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
415+
this.reject_in_isolation("SetCurrentDirectoryW", reject_with)?;
416+
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
417+
this.set_last_error_from_io_error(err)?;
418+
419+
return Ok(0);
420+
}
397421

398422
let path = this.read_path_from_wide_str(this.read_scalar(path_op)?.check_init()?)?;
399423

0 commit comments

Comments
 (0)