Using max stack limit to determine current js engine and revision

# Brandon Benvie (9 years ago)

I submitted this as a potential security vulnerability to the Chromium bug list but it didn't seem to register there. It's not a vulnerability in that it has an imminent impact on anything, but I would still classify it as one because of the fundamental level its active at, it's near ubiquitous presence in past and present version of JS engines, its incredible specificity in most cases, and the fact that it's completely impossible to prevent without changing at the engine level.

To sum up: JS by nature of not having tail call optimization has maximum stack frames as a matter of course and it's generally an early fail. The interesting thing is that with a little bit of try catch you can determine the maximum stack size before the limit was reached. A novel triviality on its own but the really interesting thing is a combination of two odd facts: the maximum stack size varies on almost every js engine between may minor versions (and of course against other engines), and yet it's also extremely stable and predictable for a given version.

The ultimate result is that it's trivial to determine to a very precise range not only what engine you're running or even what version it is, but the minor version as well. And the only requirements for you to be able to do this are try catch and recursively calling your own function, so it's (as far as I know) impossible to prevent in any sand boxing effort that I'm aware of.

It's not really a security vulnerability on its own, but it does mean that as of current it's impossible to prevent executing JavaScript from being able to precisely determine under what conditions its running in almost all cases without being able to patch the engine. Being able to target specific versions is a key tool though in the arsenal of exploiting vulnerabilities.

This was the reproduction case I had, specifically for Chrome, but it works the same in all but ont case

REPRODUCTION CASE setTimeout(function(){var i=0; try{(function m(){++i&&m()}())}catch(e){console.log(i)}}, 1)

Which produces (give or take a few); 17 Release (v8 3.7 at 10585): 18762 18 Dev (v8 3.8 at 10945): 18747 19 Canary (v8 3.9 at 10875): 18751 node (v8 3.9 at 10896): 18703

The code in question for some reason simlpy causes my current version of Firefox to infinitely execute. I do know this works in spidermonkey itself so I'm not sure what the issue is currently. I'm sure it'd just requiring rejiggering it to prevent whatever is preventing the stack frame bailout.

The interesting thing is that this in most cases is stable across platforms and archtiectures. And in the few cases its not, that's just more information you have exposed and capturable.

# Mark S. Miller (9 years ago)

as a vulnerability, I admit it doesn't register much on me either. Such version detection is not something I imagined we could prevent and it has never really been on my radar to try. Nevertheless, your experiments are interesting.

But the catchable out-of-memory errors you observe do cause a different kind of vulnerability: < code.google.com/p/google-caja/issues/detail?id=460>. Your

demonstration of the predictable regularity of these errors does make this vulnerability more exploitable. Joe-E addresses this my making all non-deterministic errors, including java.lang.OutOfMemoryErrors, non-catchable, so that no computation can occur in the (likely corrupt) computation state following such an occurrence. See < www.eros-os.org/pipermail/e-lang/2007-January/011817.html> for one

of many discussions. This also echoes the Erlang fail-fast philosophy.

As an ES5 library without code verification or rewriting, there's nothing that SES can reasonably do about this. Perhaps this is something to address eventually once SES is made into a de jure standard.

# Brandon Benvie (9 years ago)

As an ES5 library without code verification or rewriting, there's nothing that SES can reasonably do about this. Perhaps this is something to address eventually once SES is made into a de jure standard.

That's the part made me even give it a second thought. The answer I got on the Chromium report was telling and also makes sense given who's answering "browsers have a navigator object that says all of this anyway" essentially.

I was thinking from the es-discuss angle, not the chromium or browser one, in that this is kind of a language issue because it's a required functionality in the language that's also unspecified and has resulted in a universal property that makes it impossible to fully sandbox code you run from leaking this information. The efforts of SES were the angle I was thinking of as it shows there's definitely a use case impacted by this kind of engine level issue.

# David Bruant (9 years ago)

Le 10/03/2012 23:03, Brandon Benvie a écrit :

I submitted this as a potential security vulnerability to the Chromium bug list but it didn't seem to register there. It's not a vulnerability in that it has an imminent impact on anything, but I would still classify it as one because of the fundamental level its active at, it's near ubiquitous presence in past and present version of JS engines, its incredible specificity in most cases, and the fact that it's completely impossible to prevent without changing at the engine level.

To sum up: JS by nature of not having tail call optimization has maximum stack frames as a matter of course and it's generally an early fail.

I'm not sure I understand what tail call optimization has to do with maximum stack frame.

Each function call needs some memory for the scope and the function arguments. Memory is limited. Hence there has to be a limit in the number of nested function calls.

Tail call optimization helps in reducing the number of contexts used under circumstances, but it doesn't change the above reasoning, does it?

# Lasse Reichstein (9 years ago)

On Sun, Mar 11, 2012 at 9:58 AM, David Bruant <bruant.d at gmail.com> wrote:

I'm not sure I understand what tail call optimization has to do with maximum stack frame.

Each function call needs some memory for the scope and the function arguments. Memory is limited. Hence there has to be a limit in the number of nested function calls.

Tail call optimization helps in reducing the number of contexts used under circumstances, but it doesn't change the above reasoning, does it?

With tail-call optimization, a tail call from a function will completely remove its frame and reuse it for the next function called. Even the return address is reused. You can really have infinitely many recursive calls with tail-call optimization, and it is one of the ways to make an unbounded loop in functional languages.

Now, for this problem it is indeed irrelevant, since we are talking about a malicious attacker, and he'll just make a function that is recursive, but not tail-recursive (e.g., function foo() { depth++; return 1+foo(); }) which needs to return to each calling function.

# David Bruant (8 years ago)

Le 10/03/2012 23:03, Brandon Benvie a écrit :

It's not really a security vulnerability on its own, but it does mean that as of current it's impossible to prevent executing JavaScript from being able to precisely determine under what conditions its running in almost all cases without being able to patch the engine. Being able to target specific versions is a key tool though in the arsenal of exploiting vulnerabilities.

I have been thinking about this for days and still can't really say if it's true or false. In a way, this kind of information retrieval is a form of browser sniffing. If I was in possession of different hacks, I would just try them out in a "feature testing" fashion. Instead of figuring out which platform I'm on, I just try to see if the platform I'm running on is vulnerable to breach I have found. Worst case, I'm not on the right platform and my attempt fail. It couldn't work anyway.

Mark Miller:

As an ES5 library without code verification or rewriting, there's nothing that SES can reasonably do about this. Perhaps this is something to address eventually once SES is made into a de jure standard.

I see 2 cases:

  1. The untrusted code is being run in a sandbox. This sandbox can decide to do some rewriting before running the code and I'm confident the stack size and even message queue size can be controlled by the sandbox.
  2. The untrusted code runs un-sandboxed like if it was trusted code (thanks to an XSS or whatever reason). In that case, it often means that you're in bigger trouble than the untrusted code being able to figure out the specific version of your platform.

So I don't see a use for the language to address this particular issue.

# Brandon Benvie (8 years ago)

It's not something I've seen in browsers and what you said makes sense. But in other arenas being able to ascertain the version of software is often an important step along the route to gaining improper access. I'm not a security expert (I'm sure someone else here is) but it's often the first first step taken in the discussions of means and methods I've read in compromising say a server. That's the reason I mentioned it but I also have no specific reason to believe JavaScript engines would follow that line of attack.

# Mark S. Miller (8 years ago)

On Tue, Mar 20, 2012 at 12:51 PM, David Bruant <bruant.d at gmail.com> wrote:

Mark Miller:

As an ES5 library without code verification or rewriting, there's nothing that SES can reasonably do about this. Perhaps this is something to address eventually once SES is made into a de jure standard. I see 2 cases:

  1. The untrusted code is being run in a sandbox. This sandbox can decide to do some rewriting before running the code and I'm confident the stack size and even message queue size can be controlled by the sandbox.

Why are you confident? What do you mean by "controlled"? Limiting the total amount of memory is not the issue.

The issue I worry about, as indicated by my previous links to < code.google.com/p/google-caja/issues/detail?id=460> and <

www.eros-os.org/pipermail/e-lang/2007-January/011817.html>, is an

attacker causing a defender to lose integrity by causing the defender to abort forward execution at an unexpected and vulnerable point, but for execution to then proceed among the defender's objects after this occurrence.

To stress the problem, I like to pose the following puzzle to Java programmers:

You are given a properly formed conventional doubly linked list. Write a function which, when called, will attempt to splice in an element. a) If no exceptional conditions occur, the function must return in a state where the splice succeeded. b) Under all exceptional conditions allowed by the Java and JVM contracts, if the function exits (whether by return or throwing), it must exit in a state in which the doubly linked list is still well formed. The splice may or may not have occurred. You may assume only one thread is executing.

The reason that all conventional answers to this challenge fail is that a JVM VirtualMachineError (including an OutOfMemoryError) can be thrown between any pair of instructions. In order to succeed at goal #a, at some point one of the doubly linked list's pointers need to be updated, leaving the list ill formed. After that, a VirtualMachineError may be thrown. We might put the whole thing into a try/catch or try/finally to try to restore the list to its original state in this circumstance, but another VirtualMachineError may prevent any progress there as well. The only solution is for the function not to exit once the shared data structure it is to update might be in an ill-formed state.

Getting back to SES, SES as a library on ES5 without translation or verification cannot solve this puzzle. With translation, verification, or direct implementation, it can do what Joe-E and Erlang do: abort the process, abandoning all possibly corrupt state. In the browser context, we could perhaps force a reload.

  1. The untrusted code runs un-sandboxed like if it was trusted code (thanks to an XSS or whatever reason). In that case, it often means that you're in bigger trouble than the untrusted code being able to figure out the specific version of your platform.

I'm not worried about untrusted code being able to figure out what platform it's on. I do not think it useful to try to address that threat model, including for the reasons you state.

So I don't see a use for the language to address this particular issue.

depends what issue you're worried about ;).

# Andrea Giammarchi (8 years ago)

not sure if it's setTimeout problem but this snippet

(function (Function, MAX_EXECUTION_STACK) { if (MAX_EXECUTION_STACK in Function) return; Function[MAX_EXECUTION_STACK] = function (i) { try { (function max(){ ++i && max(); }()); } catch(o_O) { return i; } }(0); }(Function, "MAX_EXECUTION_STACK")); // browser dependentalert(Function.MAX_EXECUTION_STACK);

from here: webreflection.blogspot.com/2012/02/jsonstringify-recursion-max-execution.html

never gave me problems.

br

# David Bruant (8 years ago)

Le 20/03/2012 16:28, Mark S. Miller a écrit :

On Tue, Mar 20, 2012 at 12:51 PM, David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>> wrote:

Mark Miller:
> As an ES5 library without code verification or rewriting, there's
> nothing that SES can reasonably do about this. Perhaps this is
> something to address eventually once SES is made into a de jure
standard.
I see 2 cases:
1) The untrusted code is being run in a sandbox. This sandbox can
decide
to do some rewriting before running the code and I'm confident the
stack
size and even message queue size can be controlled by the sandbox.

Why are you confident?

Mostly because of what you said below: "With translation, verification, or direct implementation, it can do what Joe-E and Erlang do: abort the process, abandoning all possibly corrupt state."

What do you mean by "controlled"? Limiting the total amount of memory is not the issue.

The original issue came from the fact that untrusted code can get information out of the maximum call stack size. With rewriting, it could be possible to set the call stack size of untrusted code to an aribitrary number that doesn't leak information on the platform (obviously "arbitrary" under the limitation of the platform itself)

The issue I worry about, as indicated by my previous links to code.google.com/p/google-caja/issues/detail?id=460 and www.eros-os.org/pipermail/e-lang/2007-January/011817.html, is an attacker causing a defender to lose integrity by causing the defender to abort forward execution at an unexpected and vulnerable point, but for execution to then proceed among the defender's objects after this occurrence.

I didn't send my message for days because I was trying to think about how to solve this particular issue.

(...) Getting back to SES, SES as a library on ES5 without translation or verification cannot solve this puzzle. With translation, verification, or direct implementation, it can do what Joe-E and Erlang do: abort the process, abandoning all possibly corrupt state.

And indeed, aborting sounds like that's what should be done. Although implementations currently agree on a catchable error for maximum stack frame, it is not part of ECMAScript. Alongside with tail call optimisation, this could (and probably should) be part of an optional part of the next standard.

In the browser context, we could perhaps force a reload.

I think that the rationale behind continuing the execution of JavaScript even when a maximum call number has been reached is that, in some cases, it may not corrupt the state and the user can continue to interact with the page even if there has been one exception. Unfortunately, I don't really know when it's true and when it isn't.

Forcing a reload of the page would be terrible from a UX persective. A more conservative approach could be to disable JavaScript in the page (as with the NoScript Firefox add-on) and display a message like "an error happened on this website. It may not be as interactive.". This way, links and forms are still available. It would also prevent reloading a page that would reach the maximum call number when run (causing permanent reloading).

Alternatively, a script could be provided as "backup". If the browser disables JavaScript, it sweeps all the objects, pending messages, etc. in the ECMAScript environment (except the minimum to provide what HTML+CSS would provide) and runs the backup script in a fresh environment (+ previous DOM). Since the environement is safe, there is no state corruption. There might be in the DOM, but the backup script can check for that. I don't really know how the backup is safely provided. First run first serve, probably?

A vat contains a stack and a queue. Although these are used for different purposes, it seems that the error handling mechanism used when abusing of them is different. The stack throws a catchable error while the exhausting the queue will result in a a browser crash out of memory exhaustion. I wonder what makes both so different. My guess is that it's only a matter of optimization. I would guess that a JavaScript stack is mapped into a "system stack". While I see the benefit from this at small scale, I don't understand why the JavaScript call stack could not continue in the process heap so that recursive calls could continue until memory exhaustion. It makes me curious.