LibJS: Fix UAF in ECMAScriptFunctionObject::internal_construct

Currently, we create `this_argument` with
`ordinary_create_from_constructor`, then we use `arguments_list` to
build the callee_context.

The issue is we don't properly model the side-effects of
`ordinary_create_from_constructor`, if `new_target` is a proxy object
then when we `get` the prototype, arbitrary javascript can run.

This javascript could perform a function call with enough arguments to
reallocate the interpreters m_argument_values_buffer vector. This is
dangerous and leads to a use-after-free, as our stack frame maintains a
pointer to m_argument_values_buffer (`arguments_list`).
This commit is contained in:
Jess 2025-03-18 00:13:20 +13:00 committed by Jelle Raaijmakers
parent b8fa355a21
commit f5a6704219
Notes: github-actions[bot] 2025-03-19 09:31:57 +00:00
2 changed files with 44 additions and 11 deletions

View file

@ -439,6 +439,17 @@ ThrowCompletionOr<GC::Ref<Object>> ECMAScriptFunctionObject::internal_construct(
{
auto& vm = this->vm();
auto callee_context = ExecutionContext::create();
// Non-standard
callee_context->arguments.ensure_capacity(max(arguments_list.size(), m_formal_parameters.size()));
callee_context->arguments.append(arguments_list.data(), arguments_list.size());
callee_context->passed_argument_count = arguments_list.size();
if (arguments_list.size() < m_formal_parameters.size()) {
for (size_t i = arguments_list.size(); i < m_formal_parameters.size(); ++i)
callee_context->arguments.append(js_undefined());
}
// 1. Let callerContext be the running execution context.
// NOTE: No-op, kept by the VM in its execution context stack.
@ -453,17 +464,6 @@ ThrowCompletionOr<GC::Ref<Object>> ECMAScriptFunctionObject::internal_construct(
this_argument = TRY(ordinary_create_from_constructor<Object>(vm, new_target, &Intrinsics::object_prototype, ConstructWithPrototypeTag::Tag));
}
auto callee_context = ExecutionContext::create();
// Non-standard
callee_context->arguments.ensure_capacity(max(arguments_list.size(), m_formal_parameters.size()));
callee_context->arguments.append(arguments_list.data(), arguments_list.size());
callee_context->passed_argument_count = arguments_list.size();
if (arguments_list.size() < m_formal_parameters.size()) {
for (size_t i = arguments_list.size(); i < m_formal_parameters.size(); ++i)
callee_context->arguments.append(js_undefined());
}
// 4. Let calleeContext be PrepareForOrdinaryCall(F, newTarget).
// NOTE: We throw if the end of the native stack is reached, so unlike in the spec this _does_ need an exception check.
TRY(prepare_for_ordinary_call(*callee_context, &new_target));

View file

@ -0,0 +1,33 @@
test("Proxied constructor should handle argument_buffer reallocation during prototype get()", () => {
function foo() {}
let handler = {
get() {
// prettier-ignore
foo(
// make extra sure we trigger a reallocation
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41
);
return null;
},
};
function Construct() {
// later use dangling pointer
console.log(arguments);
}
let ConstructProxy = new Proxy(Construct, handler);
new ConstructProxy(0x1);
});