microsoft/ClearScript

Need Strategies to Prevent ClearScript V8 Crashes Due to Large Memory Allocation in Scripts

Closed this issue · 1 comments

Hello ClearScript Team,

We are encountering a significant challenge with the ClearScript V8 engine in our web application. Our application allows users to execute JavaScript scripts through a UI interface. Unfortunately, we've found that this feature can be exploited to write scripts that allocate excessive memory, ultimately leading to application crashes. An example of such a script is:

let array = new Array(2048*1024*1024);
for (let i = 0; i < array.length; i++) {
    array[i] = 0;
}

This script attempts to create a very large array, which, after some time, causes the application to crash. The crash is evidenced by the following error in our event logs:

Faulting application name: w3wp.exe, version: 10.0.17763.1, time stamp: 0xcfdb13d8
Faulting module name: ClearScriptV8.win-x64.dll, version: 7.1.7.0, time stamp: 0x617042a0
Exception code: 0x80000003
Fault offset: 0x00000000000443bb
Faulting process id: 0x6248
Faulting application start time: 0x01da4527a6e6ee4a
Faulting application path: c:\windows\system32\inetsrv\w3wp.exe
Faulting module path: F:\inetpub\sites\pull12212\runtimes\win-x64\native\ClearScriptV8.win-x64.dll

We have tried to mitigate this issue by setting the maximum heap size and adjusting the heap size sample interval as follows, but these measures have not resolved the problem:

using var runtime = new V8Runtime(new V8RuntimeConstraints());
runtime.MaxHeapSize = (UIntPtr)1073741824;  // 1 GB
runtime.HeapSizeSampleInterval = TimeSpan.MinValue;
using (var engine = runtime.CreateScriptEngine(V8ScriptEngineFlags.EnableDateTimeConversion))
{
//
}

Unfortunately, these settings do not seem to prevent the application crash when the script attempts to allocate excessive memory. We are seeking your guidance and would like to know:

  1. What best practices do you recommend for handling situations where a script might consume excessive resources, potentially leading to a crash of the host application?

  2. Is it possible to catch such exceptions or errors gracefully within the application to avoid a complete crash?

We would greatly appreciate any advice or suggestions you can provide to help us enhance the stability and security of our application.

Hello @deimosowen,

While the combination of runtime constraints and heap size monitoring can help catch runaway scripts (especially with the recently added HeapExpansionMultiplier). it doesn't provide full isolation.

One problem is that V8 has internal limits that the host can't specify or monitor. For example, a size limit for objects on the heap restricts array lengths to under 128 million elements and places unknown constraints on other objects. Exceeding that limit causes a crash no matter how far V8 is from actually saturating the heap.

Another issue is that V8 is very complex and can have exploitable bugs. The V8 team is working on the V8 Sandbox, a feature that should prevent host memory corruption, but our understanding is that it won't help with the arbitrary limits mentioned above.

The bottom line is that V8 does not provide a bulletproof in-process script execution environment. According to the V8 team, running an untrusted script is just like calling an untrusted native library. Full isolation is only achievable by running scripts in a separate process – like Chromium does. Someone clarified the V8 team's position here: "I can understand that there are scenarios outside of Chrome where a slower execution handling OOM more gracefully would be preferable, but this is outside v8's scope."

However, that was a while back, and the V8 team has made some concessions since then. For example, a script can no longer blow up the process by creating a very large string. Various mitigations for untrusted scripts have been implemented.

If you're worried about simple buggy scripts rather than sophisticated attacks, there are things you could do to harden your host against specific scenarios. For example, you could proxy the Array object to customize its behavior:

engine.Execute(@"
    Array = new Proxy(Array, {
        construct(target, args, newTarget) {
            const maxLength = 100000000; // 100 million
            if (args.length == 1 && Number.isInteger(args[0]) && args[0] > maxLength) {
                throw new RangeError('Array length too large');
            }
            return Reflect.construct(target, args, newTarget);
        }
    });
");

There are other ways, of course, to exceed the array size limit, but they all should be patchable via similar techniques.

Good luck!