Skip to content

Commit fee808f

Browse files
authored
Merge pull request #2091 from harehare/feat/allow-env-sandbox
✨ feat(mq-lang,mq-run): add --allow-env sandbox flag
2 parents 9b94443 + d2cfe58 commit fee808f

11 files changed

Lines changed: 342 additions & 117 deletions

File tree

crates/mq-lang/src/ast/parser.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> {
783783
token_id: self.token_arena.alloc(Shared::clone(token)),
784784
expr: io_context::current()
785785
.env_var(s)
786-
.map_err(|_| SyntaxError::EnvNotFound((**token).clone(), SmolStr::new(s)))
786+
.map_err(|e| match e {
787+
crate::io::IoError::PermissionDenied(_) => {
788+
SyntaxError::EnvNotAllowed((**token).clone(), SmolStr::new(s))
789+
}
790+
_ => SyntaxError::EnvNotFound((**token).clone(), SmolStr::new(s)),
791+
})
787792
.map(|s| Shared::new(Expr::Literal(Literal::String(s.to_owned()))))?,
788793
})),
789794
TokenKind::Eof => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)),
@@ -8845,6 +8850,9 @@ mod tests {
88458850
#[test]
88468851
fn test_parse_env() {
88478852
unsafe { std::env::set_var("MQ_TEST_VAR", "test_value") };
8853+
let _guard = io_context::scoped(Shared::new(
8854+
crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true),
8855+
));
88488856

88498857
let mut arena = Arena::new(10);
88508858
let tokens = [
@@ -8877,6 +8885,10 @@ mod tests {
88778885

88788886
#[test]
88798887
fn test_parse_env_not_found() {
8888+
let _guard = io_context::scoped(Shared::new(
8889+
crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true),
8890+
));
8891+
88808892
let mut arena = Arena::new(10);
88818893
let token = Shared::new(Token {
88828894
range: Range::default(),
@@ -8904,6 +8916,9 @@ mod tests {
89048916
#[test]
89058917
fn test_parse_env_in_arguments() {
89068918
unsafe { std::env::set_var("MQ_ARG_TEST", "env_arg_value") };
8919+
let _guard = io_context::scoped(Shared::new(
8920+
crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true),
8921+
));
89078922

89088923
let mut arena = Arena::new(10);
89098924
let tokens = [

crates/mq-lang/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ impl Diagnostic for Error {
211211
InnerError::Syntax(SyntaxError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
212212
"Environment variable '{env}' not found. Did you forget to set it?"
213213
))),
214+
InnerError::Syntax(SyntaxError::EnvNotAllowed(_, env)) => Some(Cow::Owned(format!(
215+
"Environment variable '{env}' is not permitted. Pass --allow-env to permit it."
216+
))),
214217
InnerError::Syntax(SyntaxError::UnexpectedToken(token)) if token.kind == TokenKind::Eof => {
215218
Some(Cow::Borrowed(
216219
"The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
@@ -350,6 +353,9 @@ impl Diagnostic for Error {
350353
InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, env))) => {
351354
Some(Cow::Owned(format!("Environment variable '{env}' not found in module.")))
352355
}
356+
InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotAllowed(_, env))) => Some(Cow::Owned(
357+
format!("Environment variable '{env}' is not permitted in module. Pass --allow-env to permit it."),
358+
)),
353359
InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(token)))
354360
if token.kind == TokenKind::Eof =>
355361
{
@@ -446,6 +452,7 @@ impl Diagnostic for Error {
446452
InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => "invalid assignment target",
447453
InnerError::Syntax(SyntaxError::UnknownSelector(_)) => "unknown selector",
448454
InnerError::Syntax(SyntaxError::EnvNotFound(_, _)) => "environment variable not found",
455+
InnerError::Syntax(SyntaxError::EnvNotAllowed(_, _)) => "environment variable not allowed",
449456
InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => "parameter without default",
450457
InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => "parameter with default value",
451458
InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => "misplaced variadic parameter",

crates/mq-lang/src/error/syntax.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ pub enum SyntaxError {
99
/// An environment variable was not found.
1010
#[error("Undefined environment variable `{1}`")]
1111
EnvNotFound(Token, SmolStr),
12+
/// An environment variable exists but isn't permitted by `--allow-env`.
13+
#[error("Access to environment variable `{1}` is not allowed")]
14+
EnvNotAllowed(Token, SmolStr),
1215
/// An unexpected token was encountered during parsing.
1316
#[error("Unexpected token `{}`", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
1417
UnexpectedToken(Token),
@@ -68,6 +71,7 @@ impl SyntaxError {
6871
pub fn token(&self) -> Option<&Token> {
6972
match self {
7073
SyntaxError::EnvNotFound(token, _) => Some(token),
74+
SyntaxError::EnvNotAllowed(token, _) => Some(token),
7175
SyntaxError::UnexpectedToken(token) => Some(token),
7276
SyntaxError::UnexpectedEOFDetected(_) => None,
7377
SyntaxError::InsufficientTokens(token) => Some(token),
@@ -103,6 +107,7 @@ mod tests {
103107

104108
#[rstest]
105109
#[case(SyntaxError::EnvNotFound(eof_token(), "VAR".into()), true)]
110+
#[case(SyntaxError::EnvNotAllowed(eof_token(), "VAR".into()), true)]
106111
#[case(SyntaxError::UnexpectedToken(eof_token()), true)]
107112
#[case(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)), false)]
108113
#[case(SyntaxError::InsufficientTokens(eof_token()), true)]

crates/mq-lang/src/eval.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,11 +1170,14 @@ impl<T: ModuleResolver, IO: Io> Evaluator<T, IO> {
11701170
acc.push_str(&value.to_string());
11711171
}
11721172
ast::StringSegment::Env(env_var) => {
1173-
acc.push_str(&io_context::current().env_var(env_var).map_err(|_| {
1174-
RuntimeError::EnvNotFound(
1175-
(*get_token(Shared::clone(&self.token_arena), token_id)).clone(),
1176-
env_var.clone(),
1177-
)
1173+
acc.push_str(&io_context::current().env_var(env_var).map_err(|e| {
1174+
let token = (*get_token(Shared::clone(&self.token_arena), token_id)).clone();
1175+
match e {
1176+
crate::io::IoError::PermissionDenied(msg) => {
1177+
RuntimeError::Runtime(token, msg.into_owned())
1178+
}
1179+
_ => RuntimeError::EnvNotFound(token, env_var.clone()),
1180+
}
11781181
})?);
11791182
}
11801183
ast::StringSegment::Self_ => {
@@ -1314,11 +1317,12 @@ impl<T: ModuleResolver, IO: Io> Evaluator<T, IO> {
13141317
if expr_str == constants::identifiers::SELF {
13151318
acc.push_str(&runtime_value.to_string());
13161319
} else if let Some(var) = expr_str.strip_prefix('$') {
1317-
acc.push_str(
1318-
&io_context::current()
1319-
.env_var(var)
1320-
.map_err(|_| RuntimeError::EnvNotFound((**token).clone(), var.into()))?,
1321-
);
1320+
acc.push_str(&io_context::current().env_var(var).map_err(|e| match e {
1321+
crate::io::IoError::PermissionDenied(msg) => {
1322+
RuntimeError::Runtime((**token).clone(), msg.into_owned())
1323+
}
1324+
_ => RuntimeError::EnvNotFound((**token).clone(), var.into()),
1325+
})?);
13221326
} else {
13231327
let value = self.eval_debug_expr(expr_str, token, env)?;
13241328
acc.push_str(&value.to_string());
@@ -7930,10 +7934,9 @@ mod tests {
79307934
#[case] program: Program,
79317935
#[case] expected: Result<Vec<RuntimeValue>, InnerError>,
79327936
) {
7933-
assert_eq!(
7934-
Evaluator::new(DefaultModuleLoader::default(), token_arena).eval(&program, runtime_values.into_iter()),
7935-
expected
7936-
);
7937+
let mut evaluator = Evaluator::new(DefaultModuleLoader::default(), token_arena);
7938+
evaluator.set_io(Shared::new(SandboxedIo::new(NativeIo::default()).allow_env(true)));
7939+
assert_eq!(evaluator.eval(&program, runtime_values.into_iter()), expected);
79377940
}
79387941

79397942
#[test]
@@ -8550,6 +8553,7 @@ mod debugger_tests {
85508553
#[test]
85518554
fn test_logpoint_message_supports_literal_braces_and_env_vars() {
85528555
let mut engine = crate::DefaultEngine::default();
8556+
engine.set_io(Shared::new(SandboxedIo::new(NativeIo::default()).allow_env(true)));
85538557
let log_points = Shared::new(SharedCell::new(Vec::new()));
85548558
engine.set_debugger_handler(Box::new(RecordingDebuggerHandler {
85558559
breakpoint_hits: Shared::default(),

crates/mq-lang/src/io.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ mod sandboxed;
2424

2525
pub use error::IoError;
2626
pub use native::NativeIo;
27-
pub use sandboxed::{PathAccess, SandboxedIo};
27+
pub use sandboxed::{EnvAccess, PathAccess, SandboxedIo};
2828

2929
/// `pub` here (rather than gated by the `mock-io` feature) would still not leak `MemIo`
3030
/// externally, since the `io` module itself is private to the crate (see `mod io;` in

0 commit comments

Comments
 (0)