Skip to content

Commit

Permalink
LibJS: Implement the Dynamic Code Brand Checks stage 3 proposal
Browse files Browse the repository at this point in the history
This is an active proposal at stage 3 of the TC39 proposal process.
See: https://tc39.es/proposal-dynamic-code-brand-checks/
See: https://github.com/tc39/proposal-dynamic-code-brand-checks

This proposal essentially adds support for the TrustedScript type from
the Trusted Types specification to eval and Function. This in turn
pipes support for the type into the CSP hook to check if the CSP allows
dynamic code compilation.

However, it currently doesn't support ShadowRealms, so the
implementation here is a close approximation, using PerformEval as the
basis.
See: tc39/proposal-dynamic-code-brand-checks#19

This is required to support the new function signature for the CSP
hook, and will allow us to slot in Trusted Types support in the future.
  • Loading branch information
Lubrsi committed Dec 10, 2024
1 parent 349da2b commit 61e7272
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 108 deletions.
3 changes: 0 additions & 3 deletions Libraries/LibJS/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,6 @@ class Parser {
bool try_parse_arrow_function_expression_failed;
};

// Needs to mess with m_state, and we're not going to expose a non-const getter for that :^)
friend ThrowCompletionOr<GC::Ref<ECMAScriptFunctionObject>> FunctionConstructor::create_dynamic_function(VM&, FunctionObject&, FunctionObject*, FunctionKind, ReadonlySpan<String> parameter_args, String const& body_arg);

static Parser parse_function_body_from_string(ByteString const& body_string, u16 parse_options, Vector<FunctionParameter> const& parameters, FunctionKind kind, FunctionParsingInsights&);

private:
Expand Down
99 changes: 62 additions & 37 deletions Libraries/LibJS/Runtime/AbstractOperations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -502,36 +502,61 @@ Object* get_super_constructor(VM& vm)
}

// 19.2.1.1 PerformEval ( x, strictCaller, direct ), https://tc39.es/ecma262/#sec-performeval
// 3 PerformEval ( x, strictCaller, direct ), https://tc39.es/proposal-dynamic-code-brand-checks/#sec-performeval
ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller, EvalMode direct)
{
// 1. Assert: If direct is false, then strictCaller is also false.
VERIFY(direct == EvalMode::Direct || strict_caller == CallerMode::NonStrict);

// 2. If Type(x) is not String, return x.
if (!x.is_string())
GC::Ptr<PrimitiveString> code_string;

// 2. If x is a String, then
if (x.is_string()) {
// a. Let xStr be x.
code_string = x.as_string();
}
// 3. Else if x is an Object, then
else if (x.is_object()) {
// a. Let code be HostGetCodeForEval(x).
auto code = vm.host_get_code_for_eval(x.as_object());

// b. If code is a String, let xStr be code.
if (code) {
code_string = code;
}
// c. Else, return x.
else {
return x;
}
}
// 4. Else,
else {
// a. Return x.
return x;
auto& code_string = x.as_string();
}

VERIFY(code_string);

// 3. Let evalRealm be the current Realm Record.
// 5. Let evalRealm be the current Realm Record.
auto& eval_realm = *vm.running_execution_context().realm;

// 4. NOTE: In the case of a direct eval, evalRealm is the realm of both the caller of eval and of the eval function itself.
// 5. Perform ? HostEnsureCanCompileStrings(evalRealm, « », x, direct).
TRY(vm.host_ensure_can_compile_strings(eval_realm, {}, code_string.utf8_string_view(), direct));
// 6. NOTE: In the case of a direct eval, evalRealm is the realm of both the caller of eval and of the eval function itself.
// 7. Perform ? HostEnsureCanCompileStrings(evalRealm, « », xStr, xStr, direct, « », x).
TRY(vm.host_ensure_can_compile_strings(eval_realm, {}, code_string->utf8_string_view(), code_string->utf8_string_view(), direct == EvalMode::Direct ? CompilationType::DirectEval : CompilationType::IndirectEval, {}, x));

// 6. Let inFunction be false.
// 8. Let inFunction be false.
bool in_function = false;

// 7. Let inMethod be false.
// 9. Let inMethod be false.
bool in_method = false;

// 8. Let inDerivedConstructor be false.
// 10. Let inDerivedConstructor be false.
bool in_derived_constructor = false;

// 9. Let inClassFieldInitializer be false.
// 11. Let inClassFieldInitializer be false.
bool in_class_field_initializer = false;

// 10. If direct is true, then
// 12. If direct is true, then
if (direct == EvalMode::Direct) {
// a. Let thisEnvRec be GetThisEnvironment().
auto this_environment_record = get_this_environment(vm);
Expand Down Expand Up @@ -562,7 +587,7 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
}
}

// 11. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection:
// 13. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection:
// a. Let script be ParseText(StringToCodePoints(x), Script).
// c. If script Contains ScriptBody is false, return undefined.
// d. Let body be the ScriptBody of script.
Expand All @@ -578,7 +603,7 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
.in_class_field_initializer = in_class_field_initializer,
};

Parser parser { Lexer { code_string.byte_string() }, Program::Type::Script, move(initial_state) };
Parser parser { Lexer { code_string->byte_string() }, Program::Type::Script, move(initial_state) };
auto program = parser.parse_program(strict_caller == CallerMode::Strict);

// b. If script is a List of errors, throw a SyntaxError exception.
Expand All @@ -589,22 +614,22 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,

bool strict_eval = false;

// 12. If strictCaller is true, let strictEval be true.
// 14. If strictCaller is true, let strictEval be true.
if (strict_caller == CallerMode::Strict)
strict_eval = true;
// 13. Else, let strictEval be IsStrict of script.
// 15. Else, let strictEval be IsStrict of script.
else
strict_eval = program->is_strict_mode();

// 14. Let runningContext be the running execution context.
// 15. NOTE: If direct is true, runningContext will be the execution context that performed the direct eval. If direct is false, runningContext will be the execution context for the invocation of the eval function.
// 16. Let runningContext be the running execution context.
// 17. NOTE: If direct is true, runningContext will be the execution context that performed the direct eval. If direct is false, runningContext will be the execution context for the invocation of the eval function.
auto& running_context = vm.running_execution_context();

Environment* lexical_environment;
Environment* variable_environment;
PrivateEnvironment* private_environment;

// 16. If direct is true, then
// 18. If direct is true, then
if (direct == EvalMode::Direct) {
// a. Let lexEnv be NewDeclarativeEnvironment(runningContext's LexicalEnvironment).
lexical_environment = new_declarative_environment(*running_context.lexical_environment);
Expand All @@ -615,7 +640,7 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
// c. Let privateEnv be runningContext's PrivateEnvironment.
private_environment = running_context.private_environment;
}
// 17. Else,
// 19. Else,
else {
// a. Let lexEnv be NewDeclarativeEnvironment(evalRealm.[[GlobalEnv]]).
lexical_environment = new_declarative_environment(eval_realm.global_environment());
Expand All @@ -627,7 +652,7 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
private_environment = nullptr;
}

// 18. If strictEval is true, set varEnv to lexEnv.
// 20. If strictEval is true, set varEnv to lexEnv.
if (strict_eval)
variable_environment = lexical_environment;

Expand All @@ -638,50 +663,50 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
variable_environment->set_permanently_screwed_by_eval();
}

// 19. If runningContext is not already suspended, suspend runningContext.
// 21. If runningContext is not already suspended, suspend runningContext.
// FIXME: We don't have this concept yet.

// 20. Let evalContext be a new ECMAScript code execution context.
// 22. Let evalContext be a new ECMAScript code execution context.
auto eval_context = ExecutionContext::create();

// 21. Set evalContext's Function to null.
// 23. Set evalContext's Function to null.
// NOTE: This was done in the construction of eval_context.

// 22. Set evalContext's Realm to evalRealm.
// 24. Set evalContext's Realm to evalRealm.
eval_context->realm = &eval_realm;

// 23. Set evalContext's ScriptOrModule to runningContext's ScriptOrModule.
// 25. Set evalContext's ScriptOrModule to runningContext's ScriptOrModule.
eval_context->script_or_module = running_context.script_or_module;

// 24. Set evalContext's VariableEnvironment to varEnv.
// 26. Set evalContext's VariableEnvironment to varEnv.
eval_context->variable_environment = variable_environment;

// 25. Set evalContext's LexicalEnvironment to lexEnv.
// 27. Set evalContext's LexicalEnvironment to lexEnv.
eval_context->lexical_environment = lexical_environment;

// 26. Set evalContext's PrivateEnvironment to privateEnv.
// 28. Set evalContext's PrivateEnvironment to privateEnv.
eval_context->private_environment = private_environment;

// NOTE: This isn't in the spec, but we require it.
eval_context->is_strict_mode = strict_eval;

// 27. Push evalContext onto the execution context stack; evalContext is now the running execution context.
// 29. Push evalContext onto the execution context stack; evalContext is now the running execution context.
TRY(vm.push_execution_context(*eval_context, {}));

// NOTE: We use a ScopeGuard to automatically pop the execution context when any of the `TRY`s below return a throw completion.
ScopeGuard pop_guard = [&] {
// FIXME: 31. Suspend evalContext and remove it from the execution context stack.
// FIXME: 33. Suspend evalContext and remove it from the execution context stack.

// 32. Resume the context that is now on the top of the execution context stack as the running execution context.
// 34. Resume the context that is now on the top of the execution context stack as the running execution context.
vm.pop_execution_context();
};

// 28. Let result be Completion(EvalDeclarationInstantiation(body, varEnv, lexEnv, privateEnv, strictEval)).
// 30. Let result be Completion(EvalDeclarationInstantiation(body, varEnv, lexEnv, privateEnv, strictEval)).
TRY(eval_declaration_instantiation(vm, program, variable_environment, lexical_environment, private_environment, strict_eval));

Optional<Value> eval_result;

// 29. If result.[[Type]] is normal, then
// 31. If result.[[Type]] is normal, then
// a. Set result to the result of evaluating body.
auto executable_result = Bytecode::Generator::generate_from_ast_node(vm, program, {});
if (executable_result.is_error())
Expand All @@ -699,11 +724,11 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
if (!result.is_empty())
eval_result = result;

// 30. If result.[[Type]] is normal and result.[[Value]] is empty, then
// 32. If result.[[Type]] is normal and result.[[Value]] is empty, then
// a. Set result to NormalCompletion(undefined).
// NOTE: Step 31 and 32 is handled by `pop_guard` above.
// 33. Return ? result.
// NOTE: Step 33 is also performed with each use of `TRY` above.
// 35. Return ? result.
// NOTE: Step 35 is also performed with each use of `TRY` above.
return eval_result.value_or(js_undefined());
}

Expand Down
13 changes: 10 additions & 3 deletions Libraries/LibJS/Runtime/AsyncFunctionConstructor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,22 @@ ThrowCompletionOr<GC::Ref<Object>> AsyncFunctionConstructor::construct(FunctionO
{
auto& vm = this->vm();

ReadonlySpan<Value> arguments = vm.running_execution_context().arguments;

ReadonlySpan<Value> parameter_args = arguments;
if (!parameter_args.is_empty())
parameter_args = parameter_args.slice(0, parameter_args.size() - 1);

// 1. Let C be the active function object.
auto* constructor = vm.active_function_object();

// 2. If bodyArg is not present, set bodyArg to the empty String.
// NOTE: This does that, as well as the string extraction done inside of CreateDynamicFunction
auto extracted = TRY(extract_parameter_arguments_and_body(vm, vm.running_execution_context().arguments));
Value body_arg = &vm.empty_string();
if (!arguments.is_empty())
body_arg = arguments.last();

// 3. Return ? CreateDynamicFunction(C, NewTarget, async, parameterArgs, bodyArg).
return TRY(FunctionConstructor::create_dynamic_function(vm, *constructor, &new_target, FunctionKind::Async, extracted.parameters, extracted.body));
return TRY(FunctionConstructor::create_dynamic_function(vm, *constructor, &new_target, FunctionKind::Async, parameter_args, body_arg));
}

}
13 changes: 10 additions & 3 deletions Libraries/LibJS/Runtime/AsyncGeneratorFunctionConstructor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,22 @@ ThrowCompletionOr<GC::Ref<Object>> AsyncGeneratorFunctionConstructor::construct(
{
auto& vm = this->vm();

ReadonlySpan<Value> arguments = vm.running_execution_context().arguments;

ReadonlySpan<Value> parameter_args = arguments;
if (!parameter_args.is_empty())
parameter_args = parameter_args.slice(0, parameter_args.size() - 1);

// 1. Let C be the active function object.
auto* constructor = vm.active_function_object();

// 2. If bodyArg is not present, set bodyArg to the empty String.
// NOTE: This does that, as well as the string extraction done inside of CreateDynamicFunction
auto extracted = TRY(extract_parameter_arguments_and_body(vm, vm.running_execution_context().arguments));
Value body_arg = &vm.empty_string();
if (!arguments.is_empty())
body_arg = arguments.last();

// 3. Return ? CreateDynamicFunction(C, NewTarget, async-generator, parameterArgs, bodyArg).
return TRY(FunctionConstructor::create_dynamic_function(vm, *constructor, &new_target, FunctionKind::AsyncGenerator, extracted.parameters, extracted.body));
return TRY(FunctionConstructor::create_dynamic_function(vm, *constructor, &new_target, FunctionKind::AsyncGenerator, parameter_args, body_arg));
}

}
69 changes: 29 additions & 40 deletions Libraries/LibJS/Runtime/FunctionConstructor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,8 @@ void FunctionConstructor::initialize(Realm& realm)
define_direct_property(vm.names.length, Value(1), Attribute::Configurable);
}

// NON-STANDARD: Exists to simplify calling CreateDynamicFunction using strong types, instead of a Value.
// Analogous to parts of the following two AO's - and basically just extracts the body and parameters as strings.
//
// 20.2.1.1 Function ( ...parameterArgs, bodyArg ), https://tc39.es/ecma262/#sec-function-p1-p2-pn-body
// 20.2.1.1.1 CreateDynamicFunction ( constructor, newTarget, kind, parameterArgs, bodyArg ), https://tc39.es/ecma262/#sec-createdynamicfunction
ThrowCompletionOr<ParameterArgumentsAndBody> extract_parameter_arguments_and_body(VM& vm, Span<Value> arguments)
{
if (arguments.is_empty())
return ParameterArgumentsAndBody {};

auto parameter_values = arguments.slice(0, arguments.size() - 1);

Vector<String> parameters;
parameters.ensure_capacity(parameter_values.size());
for (auto const& parameter_value : parameter_values)
parameters.unchecked_append(TRY(parameter_value.to_string(vm)));

auto body = TRY(arguments.last().to_string(vm));

return ParameterArgumentsAndBody {
.parameters = move(parameters),
.body = move(body),
};
}

// 20.2.1.1.1 CreateDynamicFunction ( constructor, newTarget, kind, parameterArgs, bodyArg ), https://tc39.es/ecma262/#sec-createdynamicfunction
ThrowCompletionOr<GC::Ref<ECMAScriptFunctionObject>> FunctionConstructor::create_dynamic_function(VM& vm, FunctionObject& constructor, FunctionObject* new_target, FunctionKind kind, ReadonlySpan<String> parameter_strings, String const& body_string)
ThrowCompletionOr<GC::Ref<ECMAScriptFunctionObject>> FunctionConstructor::create_dynamic_function(VM& vm, FunctionObject& constructor, FunctionObject* new_target, FunctionKind kind, ReadonlySpan<Value> parameter_args, Value body_arg)
{
// 1. If newTarget is undefined, set newTarget to constructor.
if (new_target == nullptr)
Expand Down Expand Up @@ -131,24 +106,28 @@ ThrowCompletionOr<GC::Ref<ECMAScriptFunctionObject>> FunctionConstructor::create
}

// 6. Let argCount be the number of elements in parameterArgs.
auto arg_count = parameter_strings.size();
auto arg_count = parameter_args.size();

// NOTE: Done by caller
// 7. Let parameterStrings be a new empty List.
Vector<String> parameter_strings;
parameter_strings.ensure_capacity(arg_count);

// 8. For each element arg of parameterArgs, do
// a. Append ? ToString(arg) to parameterStrings.
for (auto const& parameter_value : parameter_args) {
// a. Append ? ToString(arg) to parameterStrings.
parameter_strings.unchecked_append(TRY(parameter_value.to_string(vm)));
}

// 9. Let bodyString be ? ToString(bodyArg).
auto body_string = TRY(body_arg.to_string(vm));

// 10. Let currentRealm be the current Realm Record.
auto& realm = *vm.current_realm();

// 11. Perform ? HostEnsureCanCompileStrings(currentRealm, parameterStrings, bodyString, false).
TRY(vm.host_ensure_can_compile_strings(realm, parameter_strings, body_string, EvalMode::Indirect));

// 12. Let P be the empty String.
// 11. Let P be the empty String.
String parameters_string;

// 13. If argCount > 0, then
// 12. If argCount > 0, then
if (arg_count > 0) {
// a. Set P to parameterStrings[0].
// b. Let k be 1.
Expand All @@ -159,13 +138,16 @@ ThrowCompletionOr<GC::Ref<ECMAScriptFunctionObject>> FunctionConstructor::create
parameters_string = MUST(String::join(',', parameter_strings));
}

// 14. Let bodyParseString be the string-concatenation of 0x000A (LINE FEED), bodyString, and 0x000A (LINE FEED).
// 13. Let bodyParseString be the string-concatenation of 0x000A (LINE FEED), bodyString, and 0x000A (LINE FEED).
auto body_parse_string = ByteString::formatted("\n{}\n", body_string);

// 15. Let sourceString be the string-concatenation of prefix, " anonymous(", P, 0x000A (LINE FEED), ") {", bodyParseString, and "}".
// 16. Let sourceText be StringToCodePoints(sourceString).
// 14. Let sourceString be the string-concatenation of prefix, " anonymous(", P, 0x000A (LINE FEED), ") {", bodyParseString, and "}".
// 15. Let sourceText be StringToCodePoints(sourceString).
auto source_text = ByteString::formatted("{} anonymous({}\n) {{{}}}", prefix, parameters_string, body_parse_string);

// 16. Perform ? HostEnsureCanCompileStrings(currentRealm, parameterStrings, bodyString, sourceString, FUNCTION, parameterArgs, bodyArg).
TRY(vm.host_ensure_can_compile_strings(realm, parameter_strings, body_string, source_text, CompilationType::Function, parameter_args, body_arg));

u8 parse_options = FunctionNodeParseOptions::CheckForFunctionAndName;
if (kind == FunctionKind::Async || kind == FunctionKind::AsyncGenerator)
parse_options |= FunctionNodeParseOptions::IsAsyncFunction;
Expand Down Expand Up @@ -264,15 +246,22 @@ ThrowCompletionOr<GC::Ref<Object>> FunctionConstructor::construct(FunctionObject
{
auto& vm = this->vm();

ReadonlySpan<Value> arguments = vm.running_execution_context().arguments;

ReadonlySpan<Value> parameter_args = arguments;
if (!parameter_args.is_empty())
parameter_args = parameter_args.slice(0, parameter_args.size() - 1);

// 1. Let C be the active function object.
auto* constructor = vm.active_function_object();

// 2. If bodyArg is not present, set bodyArg to the empty String.
// NOTE: This does that, as well as the string extraction done inside of CreateDynamicFunction
auto extracted = TRY(extract_parameter_arguments_and_body(vm, vm.running_execution_context().arguments));
Value body_arg = &vm.empty_string();
if (!arguments.is_empty())
body_arg = arguments.last();

// 3. Return ? CreateDynamicFunction(C, NewTarget, normal, parameterArgs, bodyArg).
return TRY(create_dynamic_function(vm, *constructor, &new_target, FunctionKind::Normal, extracted.parameters, extracted.body));
return TRY(create_dynamic_function(vm, *constructor, &new_target, FunctionKind::Normal, parameter_args, body_arg));
}

}
Loading

0 comments on commit 61e7272

Please sign in to comment.