How V8 Calls JavaScript Functions Without Parentheses
Open DevTools, type a familiar call, and the syntax is transparent: alert(1) names a function and parentheses invoke it. The interesting puzzle begins when a small JavaScript environment allows property access and operators but rejects visible call syntax. A V8-specific trick gets around that restriction by hiding the call inside automatic conversions rather than inventing another call operator.
The chain uses V8’s non-standard Error.prepareStackTrace hook, the standard Function constructor, prototype-based string conversion, and unary plus. What does “calling a function without parentheses” really mean here? The engine still performs an ordinary function call; the source code moves that call into work JavaScript was already going to do.
The first hinge is Error.stack
V8 is the JavaScript engine used by Chromium-based browsers and Node.js. It records a stack trace, which is a list of call frames showing how execution reached a particular point. In V8, an error’s stack property normally exposes that information as a formatted string.
The formatting is often delayed until the first time stack is read:
const error = new Error;
error.stack;
Before producing the value, V8 checks whether Error.prepareStackTrace contains a custom formatter. That formatter receives two arguments: the error itself and a structured stack trace, represented as an array of call-site objects. A call-site object describes one frame, such as a function name or source location.
The surprising part is that prepareStackTrace may return more than text. V8 uses whatever the formatter returns as the value of stack, including an object or a function. That gives us a way to turn stack access into a function factory.
Let Function become the formatter
The Function constructor is a built-in that creates a new function from strings. Its final argument becomes the function body, while earlier arguments become parameter declarations. For example, this creates a function whose body prints a message:
const printMessage = Function('console.log(\'hello\')');
Now replace the stack formatter with Function itself:
Error.prepareStackTrace = Function;
const generated = new Error.stack;
Conceptually, V8 now performs a call shaped like this:
Function(errorObject, framesArray);
The error object becomes the parameter list after string conversion. For an empty Error, that conversion usually produces Error, so the generated function receives a parameter with that name. The frames array becomes the function body after it is converted to text.
The first attempt fails because the default array conversion does not produce a JavaScript program. Array.prototype.toString normally joins the array’s entries together. Stack frames may look like <anonymous>:1:12, which is useful debugger information but invalid function-body syntax. The Function constructor tries to compile that text and throws a SyntaxError.
Make the stack array supply source code
Every array inherits methods from Array.prototype. A prototype is a shared object used during property lookup, so changing a method there affects every array that uses that prototype.
For a controlled demonstration, replace the array’s string conversion with a function that returns source code:
Array.prototype.toString = =>
`console.log\\x28'generated body'\\x29`;
Error.prepareStackTrace = Function;
const generated = new Error.stack;
The hexadecimal escapes \\x28 and \\x29 represent ( and ). The text stored in the string therefore becomes:
console.log('generated body')
At this point, generated is a real function created by the Function constructor. The parentheses were not written around a function name at the point of invocation; they were introduced later as part of generated source text. This is encoding, not a security boundary.
There is another route that demonstrates prototype pollution. Prototype pollution means adding or changing properties on a shared prototype so unrelated objects inherit the altered behavior:
Object.prototype.message =
`console.log\\x28'generated body'\\x29`;
Array.prototype.toString = Error.prototype.toString;
Error.prototype.toString reads name and message from whatever object is supplied as this. An array has no own name, so the method falls back to Error. It also has no own message, so the inherited property on Object.prototype supplies the payload. The resulting string is effectively:
Error: console.log('generated body')
That is valid JavaScript because Error: is a label. The label does not control the call; it makes the prefix legal so the expression after it can run. The inherited message does not change the error argument in the same way because Error.prototype already provides an empty message property.
Unary plus becomes the trigger
Creating a function is only half the puzzle. The generated function still needs to run. The final step places it in a method that JavaScript calls automatically during object conversion:
const marker = new Error;
marker.valueOf = marker.stack;
+marker;
The assignment reads marker.stack, which invokes the custom formatter and stores the generated function as marker.valueOf. The next line applies unary plus to an object. Unary plus converts its operand to a number, and JavaScript first converts the object to a primitive value such as a string or number. For an ordinary object with no custom Symbol.toPrimitive method, the number-oriented conversion tries valueOf before toString.
That lookup finds the generated function and calls it automatically. The function body runs even though the source contains no explicit generated expression. If the body uses console.log, the unary-plus expression eventually evaluates to NaN because console.log returns undefined; the side effect has already happened.
A safer complete demonstration restores the global methods afterward:
const oldArrayToString = Array.prototype.toString;
const oldPrepareStackTrace = Error.prepareStackTrace;
try {
Array.prototype.toString = =>
`console.log\\x28'called during coercion'\\x29`;
Error.prepareStackTrace = Function;
const marker = new Error;
marker.valueOf = marker.stack;
+marker;
} finally {
Array.prototype.toString = oldArrayToString;
Error.prepareStackTrace = oldPrepareStackTrace;
}
Why this matters beyond the puzzle
This technique is not portable JavaScript. Error.stack and Error.prepareStackTrace are V8-specific extensions, and other engines may format stacks differently or omit the hook entirely. Even in V8-based environments, stack formatting is an implementation detail rather than a stable application interface.
The security lesson is broader. A filter that rejects the characters `` does not prevent function calls when property access, coercion, constructors, getters, or prototype methods remain available. At the same time, this trick is not a standalone browser compromise; code must already be running in the relevant JavaScript context, or another bug must provide control over the objects and strings involved.
Production code should avoid compiling untrusted strings with the Function constructor, avoid changing built-in prototypes, and validate keys before merging external data into ordinary objects. The cleverness here comes from composing small language rules: a stack formatter becomes a compiler, an array becomes source text, and unary plus becomes the trigger. There is no secret fifth call syntax—only a normal function call hidden inside JavaScript’s automatic machinery.
Comments (0)
No comments yet. Be the first to respond!
Leave a Comment
Your comment will be visible after review.