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.
This commit is contained in:
Smit Soni 2021-05-13 23:40:07 -07:00 committed by Smit Soni
parent f73db5ce6b
commit 892f706ce5
11 changed files with 199 additions and 43 deletions

View File

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

View File

@ -263,6 +263,9 @@ fn main() {
let mut miri_config = miri::MiriConfig::default();
let mut rustc_args = vec![];
let mut after_dashdash = false;
// If user has explicitly enabled/disabled isolation
let mut isolation_enabled: Option<bool> = None;
for arg in env::args() {
if rustc_args.is_empty() {
// Very first arg: binary name.
@ -291,7 +294,37 @@ fn main() {
miri_config.check_abi = false;
}
"-Zmiri-disable-isolation" => {
miri_config.communicate = true;
if matches!(isolation_enabled, Some(true)) {
panic!(
"-Zmiri-disable-isolation cannot be used along with -Zmiri-isolation-error"
);
} else {
isolation_enabled = Some(false);
}
miri_config.isolated_op = miri::IsolatedOp::Allow;
}
arg if arg.starts_with("-Zmiri-isolation-error=") => {
if matches!(isolation_enabled, Some(false)) {
panic!(
"-Zmiri-isolation-error cannot be used along with -Zmiri-disable-isolation"
);
} else {
isolation_enabled = Some(true);
}
miri_config.isolated_op = match arg
.strip_prefix("-Zmiri-isolation-error=")
.unwrap()
{
"abort" => miri::IsolatedOp::Reject(miri::RejectOpWith::Abort),
"hide" => miri::IsolatedOp::Reject(miri::RejectOpWith::NoWarning),
"warn" => miri::IsolatedOp::Reject(miri::RejectOpWith::Warning),
"warn-nobacktrace" =>
miri::IsolatedOp::Reject(miri::RejectOpWith::WarningWithoutBacktrace),
_ => panic!(
"-Zmiri-isolation-error must be `abort`, `hide`, `warn`, or `warn-nobacktrace`"
),
};
}
"-Zmiri-ignore-leaks" => {
miri_config.ignore_leaks = true;

View File

@ -52,6 +52,14 @@ pub enum NonHaltingDiagnostic {
CreatedCallId(CallId),
CreatedAlloc(AllocId),
FreedAlloc(AllocId),
RejectedIsolatedOp(String),
}
/// Level of Miri specific diagnostics
enum DiagLevel {
Error,
Warning,
Note,
}
/// Emit a custom diagnostic without going through the miri-engine machinery
@ -76,7 +84,7 @@ pub fn report_error<'tcx, 'mir>(
#[rustfmt::skip]
let helps = match info {
UnsupportedInIsolation(_) =>
vec![(None, format!("pass the flag `-Zmiri-disable-isolation` to disable isolation"))],
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"))],
ExperimentalUb { url, .. } =>
vec![
(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>(
let msg = e.to_string();
report_msg(
*ecx.tcx,
/*error*/ true,
DiagLevel::Error,
&if let Some(title) = title { format!("{}: {}", title, msg) } else { msg.clone() },
msg,
helps,
@ -174,18 +182,19 @@ pub fn report_error<'tcx, 'mir>(
/// Also emits a full stacktrace of the interpreter stack.
fn report_msg<'tcx>(
tcx: TyCtxt<'tcx>,
error: bool,
diag_level: DiagLevel,
title: &str,
span_msg: String,
mut helps: Vec<(Option<SpanData>, String)>,
stacktrace: &[FrameInfo<'tcx>],
) {
let span = stacktrace.first().map_or(DUMMY_SP, |fi| fi.span);
let mut err = if error {
tcx.sess.struct_span_err(span, title)
} else {
tcx.sess.diagnostic().span_note_diag(span, title)
let mut err = match diag_level {
DiagLevel::Error => tcx.sess.struct_span_err(span, title),
DiagLevel::Warning => tcx.sess.struct_span_warn(span, title),
DiagLevel::Note => tcx.sess.diagnostic().span_note_diag(span, title),
};
// Show main message.
if span != DUMMY_SP {
err.span_label(span, span_msg);
@ -303,15 +312,17 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
CreatedCallId(id) => format!("function call with id {}", id),
CreatedAlloc(AllocId(id)) => format!("created allocation with id {}", id),
FreedAlloc(AllocId(id)) => format!("freed allocation with id {}", id),
RejectedIsolatedOp(ref op) =>
format!("`{}` was made to return an error due to isolation", op),
};
report_msg(
*this.tcx,
/*error*/ false,
"tracking was triggered",
msg,
vec![],
&stacktrace,
);
let (title, diag_level) = match e {
RejectedIsolatedOp(_) =>
("operation rejected by isolation", DiagLevel::Warning),
_ => ("tracking was triggered", DiagLevel::Note),
};
report_msg(*this.tcx, diag_level, title, msg, vec![], &stacktrace);
}
});
}

View File

@ -22,6 +22,35 @@ pub enum AlignmentCheck {
Int,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RejectOpWith {
/// Isolated op is rejected with an abort of the machine.
Abort,
/// If not Abort, miri returns an error for an isolated op.
/// Following options determine if user should be warned about such error.
/// Do not print warning about rejected isolated op.
NoWarning,
/// Print a warning about rejected isolated op, with backtrace.
Warning,
/// Print a warning about rejected isolated op, without backtrace.
WarningWithoutBacktrace,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum IsolatedOp {
/// Reject an op requiring communication with the host. By
/// default, miri rejects the op with an abort. If not, it returns
/// an error code, and prints a warning about it. Warning levels
/// are controlled by `RejectOpWith` enum.
Reject(RejectOpWith),
/// Execute op requiring communication with the host, i.e. disable isolation.
Allow,
}
/// Configuration needed to spawn a Miri instance.
#[derive(Clone)]
pub struct MiriConfig {
@ -33,8 +62,8 @@ pub struct MiriConfig {
pub check_alignment: AlignmentCheck,
/// Controls function [ABI](Abi) checking.
pub check_abi: bool,
/// Determines if communication with the host environment is enabled.
pub communicate: bool,
/// Action for an op requiring communication with the host.
pub isolated_op: IsolatedOp,
/// Determines if memory leaks should be ignored.
pub ignore_leaks: bool,
/// Environment variables that should always be isolated from the host.
@ -68,7 +97,7 @@ impl Default for MiriConfig {
stacked_borrows: true,
check_alignment: AlignmentCheck::Int,
check_abi: true,
communicate: false,
isolated_op: IsolatedOp::Reject(RejectOpWith::Abort),
ignore_leaks: false,
excluded_env_vars: vec![],
args: vec![],
@ -233,7 +262,7 @@ pub fn eval_main<'tcx>(tcx: TyCtxt<'tcx>, main_id: DefId, config: MiriConfig) ->
}
SchedulingAction::ExecuteTimeoutCallback => {
assert!(
ecx.machine.communicate,
ecx.machine.communicate(),
"scheduler callbacks require disabled isolation, but the code \
that created the callback did not check it"
);

View File

@ -140,7 +140,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let mut data = vec![0; usize::try_from(len).unwrap()];
if this.machine.communicate {
if this.machine.communicate() {
// Fill the buffer using the host's rng.
getrandom::getrandom(&mut data)
.map_err(|err| err_unsup_format!("host getrandom failed: {}", err))?;
@ -391,12 +391,32 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
/// disabled. It returns an error using the `name` of the foreign function if this is not the
/// case.
fn check_no_isolation(&self, name: &str) -> InterpResult<'tcx> {
if !self.eval_context_ref().machine.communicate {
isolation_error(name)?;
if !self.eval_context_ref().machine.communicate() {
self.reject_in_isolation(name, RejectOpWith::Abort)?;
}
Ok(())
}
/// Helper function used inside the shims of foreign functions which reject the op
/// when isolation is enabled. It is used to print a warning/backtrace about the rejection.
fn reject_in_isolation(&self, op_name: &str, reject_with: RejectOpWith) -> InterpResult<'tcx> {
let this = self.eval_context_ref();
match reject_with {
RejectOpWith::Abort => isolation_abort_error(op_name),
RejectOpWith::WarningWithoutBacktrace => {
this.tcx
.sess
.warn(&format!("`{}` was made to return an error due to isolation", op_name));
Ok(())
}
RejectOpWith::Warning => {
register_diagnostic(NonHaltingDiagnostic::RejectedIsolatedOp(op_name.to_string()));
Ok(())
}
RejectOpWith::NoWarning => Ok(()), // no warning
}
}
/// Helper function used inside the shims of foreign functions to assert that the target OS
/// is `target_os`. It panics showing a message with the `name` of the foreign function
/// if this is not the case.
@ -651,7 +671,7 @@ where
throw_ub_format!("incorrect number of arguments: got {}, expected {}", args.len(), N)
}
pub fn isolation_error(name: &str) -> InterpResult<'static> {
pub fn isolation_abort_error(name: &str) -> InterpResult<'static> {
throw_machine_stop!(TerminationInfo::UnsupportedInIsolation(format!(
"{} not available when isolation is enabled",
name,

View File

@ -59,7 +59,9 @@ pub use crate::diagnostics::{
register_diagnostic, report_error, EvalContextExt as DiagnosticsEvalContextExt,
NonHaltingDiagnostic, TerminationInfo,
};
pub use crate::eval::{create_ecx, eval_main, AlignmentCheck, MiriConfig};
pub use crate::eval::{
create_ecx, eval_main, AlignmentCheck, IsolatedOp, MiriConfig, RejectOpWith,
};
pub use crate::helpers::EvalContextExt as HelpersEvalContextExt;
pub use crate::machine::{
AllocExtra, Evaluator, FrameData, MemoryExtra, MiriEvalContext, MiriEvalContextExt,

View File

@ -263,9 +263,10 @@ pub struct Evaluator<'mir, 'tcx> {
/// TLS state.
pub(crate) tls: TlsData<'tcx>,
/// If enabled, the `env_vars` field is populated with the host env vars during initialization
/// and random number generation is delegated to the host.
pub(crate) communicate: bool,
/// What should Miri do when an op requires communicating with the host,
/// such as accessing host env vars, random number generation, and
/// file system access.
pub(crate) isolated_op: IsolatedOp,
/// Whether to enforce the validity invariant.
pub(crate) validate: bool,
@ -314,7 +315,7 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
argv: None,
cmd_line: None,
tls: TlsData::default(),
communicate: config.communicate,
isolated_op: config.isolated_op,
validate: config.validate,
enforce_abi: config.check_abi,
file_handler: Default::default(),
@ -328,6 +329,10 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
exported_symbols_cache: FxHashMap::default(),
}
}
pub(crate) fn communicate(&self) -> bool {
self.isolated_op == IsolatedOp::Allow
}
}
/// A rustc InterpCx for Miri.

View File

@ -1,6 +1,7 @@
use std::convert::TryFrom;
use std::env;
use std::ffi::{OsStr, OsString};
use std::io::{Error, ErrorKind};
use rustc_data_structures::fx::FxHashMap;
use rustc_mir::interpret::Pointer;
@ -45,7 +46,7 @@ impl<'tcx> EnvVars<'tcx> {
excluded_env_vars.push("TERM".to_owned());
}
if ecx.machine.communicate {
if ecx.machine.communicate() {
for (name, value) in env::vars() {
if !excluded_env_vars.contains(&name) {
let var_ptr = match target_os {
@ -321,7 +322,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
"`getcwd` is only available for the UNIX target family"
);
this.check_no_isolation("`getcwd`")?;
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("getcwd", reject_with)?;
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
this.set_last_error_from_io_error(err)?;
return Ok(Scalar::null_ptr(&*this.tcx));
}
let buf = this.read_scalar(&buf_op)?.check_init()?;
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
}
Err(e) => this.set_last_error_from_io_error(e)?,
}
Ok(Scalar::null_ptr(&*this.tcx))
}
@ -348,7 +355,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let this = self.eval_context_mut();
this.assert_target_os("windows", "GetCurrentDirectoryW");
this.check_no_isolation("`GetCurrentDirectoryW`")?;
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("GetCurrentDirectoryW", reject_with)?;
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
this.set_last_error_from_io_error(err)?;
return Ok(0);
}
let size = u64::from(this.read_scalar(size_op)?.to_u32()?);
let buf = this.read_scalar(buf_op)?.check_init()?;
@ -370,7 +382,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
"`getcwd` is only available for the UNIX target family"
);
this.check_no_isolation("`chdir`")?;
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("chdir", reject_with)?;
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
this.set_last_error_from_io_error(err)?;
return Ok(-1);
}
let path = this.read_path_from_c_str(this.read_scalar(path_op)?.check_init()?)?;
@ -393,7 +411,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let this = self.eval_context_mut();
this.assert_target_os("windows", "SetCurrentDirectoryW");
this.check_no_isolation("`SetCurrentDirectoryW`")?;
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("SetCurrentDirectoryW", reject_with)?;
let err = Error::new(ErrorKind::NotFound, "rejected due to isolation");
this.set_last_error_from_io_error(err)?;
return Ok(0);
}
let path = this.read_path_from_wide_str(this.read_scalar(path_op)?.check_init()?)?;

View File

@ -127,7 +127,7 @@ impl FileDescriptor for io::Stdin {
) -> InterpResult<'tcx, io::Result<usize>> {
if !communicate_allowed {
// We want isolation mode to be deterministic, so we have to disallow all reads, even stdin.
helpers::isolation_error("`read` from stdin")?;
helpers::isolation_abort_error("`read` from stdin")?;
}
Ok(Read::read(self, bytes))
}
@ -662,7 +662,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let fd = this.read_scalar(fd_op)?.to_i32()?;
if let Some(file_descriptor) = this.machine.file_handler.handles.remove(&fd) {
let result = file_descriptor.close(this.machine.communicate)?;
let result = file_descriptor.close(this.machine.communicate())?;
this.try_unwrap_io_result(result)
} else {
this.handle_not_found()
@ -687,6 +687,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
// We cap the number of read bytes to the largest value that we are able to fit in both the
// host's and target's `isize`. This saves us from having to handle overflows later.
let count = count.min(this.machine_isize_max() as u64).min(isize::MAX as u64);
let communicate = this.machine.communicate();
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
trace!("read: FD mapped to {:?}", file_descriptor);
@ -696,9 +697,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let mut bytes = vec![0; count as usize];
// `File::read` never returns a value larger than `count`,
// so this cannot fail.
let result = file_descriptor
.read(this.machine.communicate, &mut bytes)?
.map(|c| i64::try_from(c).unwrap());
let result =
file_descriptor.read(communicate, &mut bytes)?.map(|c| i64::try_from(c).unwrap());
match result {
Ok(read_bytes) => {
@ -733,12 +733,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
// We cap the number of written bytes to the largest value that we are able to fit in both the
// host's and target's `isize`. This saves us from having to handle overflows later.
let count = count.min(this.machine_isize_max() as u64).min(isize::MAX as u64);
let communicate = this.machine.communicate();
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
let bytes = this.memory.read_bytes(buf, Size::from_bytes(count))?;
let result = file_descriptor
.write(this.machine.communicate, &bytes)?
.map(|c| i64::try_from(c).unwrap());
let result =
file_descriptor.write(communicate, &bytes)?.map(|c| i64::try_from(c).unwrap());
this.try_unwrap_io_result(result)
} else {
this.handle_not_found()
@ -771,9 +771,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
return Ok(-1);
};
let communicate = this.machine.communicate();
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
let result = file_descriptor
.seek(this.machine.communicate, seek_from)?
.seek(communicate, seek_from)?
.map(|offset| i64::try_from(offset).unwrap());
this.try_unwrap_io_result(result)
} else {

View File

@ -0,0 +1,20 @@
// compile-flags: -Zmiri-isolation-error=warn-nobacktrace
// normalize-stderr-test "(getcwd|GetCurrentDirectoryW)" -> "$$GET"
// normalize-stderr-test "(chdir|SetCurrentDirectoryW)" -> "$$SET"
use std::env;
use std::io::ErrorKind;
fn main() {
// Test that current dir operations return a proper error instead
// of stopping the machine in isolation mode
assert_eq!(env::current_dir().unwrap_err().kind(), ErrorKind::NotFound);
for _i in 0..3 {
assert_eq!(env::current_dir().unwrap_err().kind(), ErrorKind::NotFound);
}
assert_eq!(env::set_current_dir("..").unwrap_err().kind(), ErrorKind::NotFound);
for _i in 0..3 {
assert_eq!(env::set_current_dir("..").unwrap_err().kind(), ErrorKind::NotFound);
}
}

View File

@ -0,0 +1,4 @@
warning: `$GET` was made to return an error due to isolation
warning: `$SET` was made to return an error due to isolation