Skip to content

Commit 8c32608

Browse files
committed
Enforce strict-mode-only engine behavior and throw JS SyntaxError for 'arguments' assignment\n\n- src/core/mod.rs: mark global environment as strict by default
- src/core/eval.rs: detect assignment to 'arguments' in strict contexts and throw a SyntaxError object so JS try/catch can catch it
1 parent a63fc31 commit 8c32608

File tree

6 files changed

+136
-5
lines changed

6 files changed

+136
-5
lines changed

.github/workflows/test262.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
env:
2929
FAIL_ON_FAILURE: 'true'
3030
run: |
31-
bash ci/run_test262.sh --limit 20 --fail-on-failure --focus language
31+
bash ci/run_test262.sh --limit 30 --fail-on-failure --focus language
3232
3333
- name: Upload results
3434
if: ${{ !cancelled() }}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ with support for modern language features including ES6+ modules, async/await, B
1313

1414
## Features
1515

16+
> Note: This engine runs in strict mode only — all scripts and eval'd code are executed using ECMAScript strict semantics.
17+
18+
1619
### Core JavaScript Features (ES5-ES2020)
1720
- **Variables and Scoping**: `let`, `const`, `var` declarations with proper scope rules
1821
- **Data Types**: Numbers, strings, booleans, BigInt, symbols, objects, arrays, functions, classes

src/core/eval.rs

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
raise_eval_error, raise_reference_error,
1818
unicode::{utf8_to_utf16, utf16_to_utf8},
1919
};
20-
use crate::{Token, parse_statements, raise_type_error, tokenize};
20+
use crate::{Token, parse_statements, raise_syntax_error, raise_type_error, tokenize};
2121
use num_bigint::BigInt;
2222
use num_traits::{FromPrimitive, ToPrimitive, Zero};
2323

@@ -1024,14 +1024,134 @@ pub fn evaluate_statements_with_context<'gc>(
10241024
evaluate_statements_with_labels(mc, env, statements, labels, &[])
10251025
}
10261026

1027+
fn check_expr_for_arguments_assignment(e: &Expr) -> bool {
1028+
match e {
1029+
Expr::Assign(lhs, _rhs) => {
1030+
if let Expr::Var(name, ..) = &**lhs {
1031+
return name == "arguments";
1032+
}
1033+
false
1034+
}
1035+
Expr::Property(obj, _)
1036+
| Expr::Call(obj, _)
1037+
| Expr::New(obj, _)
1038+
| Expr::Index(obj, _)
1039+
| Expr::OptionalProperty(obj, _)
1040+
| Expr::OptionalCall(obj, _)
1041+
| Expr::OptionalIndex(obj, _) => check_expr_for_arguments_assignment(obj),
1042+
Expr::Binary(l, _, r) | Expr::Comma(l, r) | Expr::Conditional(l, r, _) => {
1043+
check_expr_for_arguments_assignment(l) || check_expr_for_arguments_assignment(r)
1044+
}
1045+
Expr::LogicalAnd(l, r) | Expr::LogicalOr(l, r) | Expr::NullishCoalescing(l, r) => {
1046+
check_expr_for_arguments_assignment(l) || check_expr_for_arguments_assignment(r)
1047+
}
1048+
Expr::AddAssign(l, r)
1049+
| Expr::SubAssign(l, r)
1050+
| Expr::MulAssign(l, r)
1051+
| Expr::DivAssign(l, r)
1052+
| Expr::ModAssign(l, r)
1053+
| Expr::BitAndAssign(l, r)
1054+
| Expr::BitOrAssign(l, r)
1055+
| Expr::BitXorAssign(l, r)
1056+
| Expr::LeftShiftAssign(l, r)
1057+
| Expr::RightShiftAssign(l, r)
1058+
| Expr::UnsignedRightShiftAssign(l, r)
1059+
| Expr::LogicalAndAssign(l, r)
1060+
| Expr::LogicalOrAssign(l, r)
1061+
| Expr::NullishAssign(l, r) => check_expr_for_arguments_assignment(l) || check_expr_for_arguments_assignment(r),
1062+
Expr::UnaryNeg(inner)
1063+
| Expr::UnaryPlus(inner)
1064+
| Expr::LogicalNot(inner)
1065+
| Expr::TypeOf(inner)
1066+
| Expr::Delete(inner)
1067+
| Expr::Void(inner)
1068+
| Expr::Await(inner)
1069+
| Expr::Yield(Some(inner))
1070+
| Expr::YieldStar(inner)
1071+
| Expr::PostIncrement(inner)
1072+
| Expr::PostDecrement(inner)
1073+
| Expr::Increment(inner)
1074+
| Expr::Decrement(inner) => check_expr_for_arguments_assignment(inner),
1075+
_ => false,
1076+
}
1077+
}
1078+
1079+
fn check_stmt_for_arguments_assignment(stmt: &Statement) -> bool {
1080+
match &*stmt.kind {
1081+
StatementKind::Expr(e) => check_expr_for_arguments_assignment(e),
1082+
StatementKind::If(if_stmt) => {
1083+
check_expr_for_arguments_assignment(&if_stmt.condition)
1084+
|| if_stmt.then_body.iter().any(check_stmt_for_arguments_assignment)
1085+
|| if_stmt
1086+
.else_body
1087+
.as_ref()
1088+
.is_some_and(|b| b.iter().any(check_stmt_for_arguments_assignment))
1089+
}
1090+
StatementKind::Block(stmts) => stmts.iter().any(check_stmt_for_arguments_assignment),
1091+
StatementKind::TryCatch(try_stmt) => {
1092+
try_stmt.try_body.iter().any(check_stmt_for_arguments_assignment)
1093+
|| try_stmt
1094+
.catch_body
1095+
.as_ref()
1096+
.is_some_and(|b| b.iter().any(check_stmt_for_arguments_assignment))
1097+
|| try_stmt
1098+
.finally_body
1099+
.as_ref()
1100+
.is_some_and(|b| b.iter().any(check_stmt_for_arguments_assignment))
1101+
}
1102+
StatementKind::FunctionDeclaration(_, _, body, _, _) => body.iter().any(check_stmt_for_arguments_assignment),
1103+
_ => false,
1104+
}
1105+
}
1106+
10271107
pub fn evaluate_statements_with_labels<'gc>(
10281108
mc: &MutationContext<'gc>,
10291109
env: &JSObjectDataPtr<'gc>,
10301110
statements: &[Statement],
10311111
labels: &[String],
10321112
own_labels: &[String],
10331113
) -> Result<ControlFlow<'gc>, EvalError<'gc>> {
1114+
// If this statement sequence begins with a "use strict" directive, mark the
1115+
// environment so eval'd code and nested parsing can behave as strict code.
1116+
if let Some(stmt0) = statements.first()
1117+
&& let StatementKind::Expr(expr) = &*stmt0.kind
1118+
&& let Expr::StringLit(s) = expr
1119+
&& utf16_to_utf8(s).as_str() == "use strict"
1120+
{
1121+
log::trace!("evaluate_statements: detected 'use strict' directive; marking env as strict");
1122+
object_set_key_value(mc, env, "__is_strict", Value::Boolean(true)).map_err(EvalError::Js)?;
1123+
}
1124+
10341125
hoist_declarations(mc, env, statements)?;
1126+
1127+
// If the environment is marked strict, scan for certain forbidden patterns
1128+
// such as assignment to the Identifier 'arguments' in function bodies which
1129+
// should be a SyntaxError under strict mode (matching Test262 expectations
1130+
// for eval'd code in strict contexts).
1131+
if let Some(is_strict_cell) = object_get_key_value(env, "__is_strict")
1132+
&& let Value::Boolean(true) = *is_strict_cell.borrow()
1133+
{
1134+
for stmt in statements {
1135+
if check_stmt_for_arguments_assignment(stmt) {
1136+
log::debug!("evaluate_statements: detected assignment to 'arguments' in function body under strict mode");
1137+
// Construct a SyntaxError object and throw it so it behaves like a JS exception
1138+
if let Some(syn_ctor_val) = object_get_key_value(env, "SyntaxError")
1139+
&& let Value::Object(syn_ctor) = &*syn_ctor_val.borrow()
1140+
&& let Some(proto_val_rc) = object_get_key_value(syn_ctor, "prototype")
1141+
&& let Value::Object(proto_ptr) = &*proto_val_rc.borrow()
1142+
{
1143+
let msg = Value::String(utf8_to_utf16("Strict mode violation: assignment to 'arguments'"));
1144+
let err_obj = crate::core::create_error(mc, Some(*proto_ptr), msg).map_err(EvalError::Js)?;
1145+
return Err(EvalError::Throw(err_obj, None, None));
1146+
}
1147+
// If we couldn't construct a SyntaxError instance for some reason, fall back
1148+
return Err(EvalError::Js(raise_syntax_error!(
1149+
"Strict mode violation: assignment to 'arguments'"
1150+
)));
1151+
}
1152+
}
1153+
}
1154+
10351155
let mut last_value = Value::Undefined;
10361156
for stmt in statements {
10371157
if let Some(cf) = eval_res(mc, stmt, &mut last_value, env, labels, own_labels)? {
@@ -6033,7 +6153,7 @@ pub fn call_native_function<'gc>(
60336153

60346154
if name == "apply" || name == "Function.prototype.apply" {
60356155
let this = this_val.ok_or_else(|| EvalError::Js(raise_eval_error!("Cannot call apply without this")))?;
6036-
log::debug!("call_native_function: apply called on this={:?}", this);
6156+
log::trace!("call_native_function: apply called on this={:?}", this);
60376157
let new_this = args.first().cloned().unwrap_or(Value::Undefined);
60386158
let arg_array = args.get(1).cloned().unwrap_or(Value::Undefined);
60396159

src/core/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ pub fn initialize_global_constructors<'gc>(mc: &MutationContext<'gc>, env: &JSOb
9898
env_set(mc, env, "Infinity", Value::Number(f64::INFINITY))?;
9999
env_set(mc, env, "eval", Value::Function("eval".to_string()))?;
100100

101+
// This engine operates in strict mode only; mark the global environment accordingly so
102+
// eval() and nested function parsing can enforce strict-mode rules unconditionally.
103+
object_set_key_value(mc, env, "__is_strict", Value::Boolean(true))?;
104+
101105
let val = Value::Function("__internal_async_step_resolve".to_string());
102106
env_set(mc, env, "__internal_async_step_resolve", val)?;
103107

src/core/parser.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,10 @@ fn parse_function_declaration(t: &[TokenData], index: &mut usize) -> Result<Stat
413413

414414
let params = parse_parameters(t, index)?;
415415

416+
// Skip any semicolons or line terminators before the function body opening brace
417+
while *index < t.len() && matches!(t[*index].token, Token::Semicolon | Token::LineTerminator) {
418+
*index += 1;
419+
}
416420
if !matches!(t[*index].token, Token::LBrace) {
417421
return Err(raise_parse_error_at(t.get(*index)));
418422
}

src/js_function.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,14 +366,14 @@ pub fn handle_global_function<'gc>(
366366
return Ok(crate::core::evaluate_statements(mc, &func_env, body)?);
367367
}
368368
Value::Object(object) => {
369-
log::debug!("Function.prototype.call on Value::Object");
369+
log::trace!("Function.prototype.call on Value::Object");
370370
if let Some(cl_rc) = object_get_key_value(&object, "__closure__")
371371
&& let Value::Closure(data) = &*cl_rc.borrow()
372372
{
373373
if args.is_empty() {
374374
return Err(raise_eval_error!("call requires a receiver"));
375375
}
376-
log::debug!("Function.prototype.call calling closure with callee={:?}", callee_for_arguments);
376+
log::trace!("Function.prototype.call calling closure with callee={:?}", callee_for_arguments);
377377
let receiver_val = args[0].clone();
378378
let forwarded = args[1..].to_vec();
379379
let evaluated_args = forwarded.to_vec();

0 commit comments

Comments
 (0)