Permalink
Browse files

first cut of the tripwire module

  • Loading branch information...
tjanczuk committed Jul 11, 2012
1 parent 820115a commit 0040e7f1d01aac9a644c24aac74a416f95c1a7bb
View
@@ -12,4 +12,5 @@ logs
results
node_modules
-npm-debug.log
+npm-debug.log
+build
View
@@ -0,0 +1,13 @@
+Copyright 2012 Tomasz Janczuk
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
View
@@ -0,0 +1,8 @@
+{
+ "targets": [
+ {
+ "target_name": "tripwire",
+ "sources": [ "src/tripwire.cc" ]
+ }
+ ]
+}
Binary file not shown.
View
@@ -0,0 +1,5 @@
+if (process.env.OS && process.env.OS.match(/windows/i))
+ module.exports = require('./native/windows/x86/tripwire');
+else
+ throw new Error('The tripwire module is currently only suppored on Windows. I do take contributions. '
+ + 'https://github.com/tjanczuk/tripwire');
View
@@ -0,0 +1,27 @@
+{
+ "name": "tripwire",
+ "author": {
+ "name": "Tomasz Janczuk <tomasz@janczuk.org>",
+ "url": "http://tomasz.janczuk.org",
+ "twitter": "tjanczuk"
+ },
+ "version": "0.1.0-pre",
+ "description": "Break out from scripts blocking node.js event loop",
+ "tags" : ["event loop", "hang"],
+ "main": "./lib/tripwire.js",
+ "engines": { "node": "0.6.x" },
+ "licenses": [ { "type": "Apache", "url": "http://www.apache.org/licenses/LICENSE-2.0" } ],
+ "dependencies": {
+ },
+ "devDependencies": {
+ "mocha": "1.2.0"
+ },
+ "homepage": "https://github.com/tjanczuk/tripwire",
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:tjanczuk/tripwire.git"
+ },
+ "bugs" : {
+ "url" : "http://github.com/tjanczuk/tripwire/issues"
+ }
+}
View
@@ -0,0 +1,165 @@
+#include <process.h>
+#include <node.h>
+#include <v8.h>
+
+using namespace v8;
+
+HANDLE scriptThread;
+DWORD tripwireThreshold;
+HANDLE tripwireThread;
+HANDLE event;
+
+void tripwireWorker(void* data)
+{
+ BOOL skipTimeCapture = FALSE;
+ ULARGE_INTEGER su, sk, eu, ek;
+ FILETIME tmp;
+
+ // This thread monitors the elapsed CPU utilization time of the node.js thread and forces V8 to terminate
+ // execution if it exceeds the preconfigured tripwireThreshold.
+
+ while (1)
+ {
+ // Unless the threshold validation logic requested to keep the current thread time utilization values,
+ // capture the current user mode and kernel mode CPU utilization time of the thread on which node.js executes
+ // application code.
+
+ if (skipTimeCapture)
+ skipTimeCapture = FALSE;
+ else
+ GetThreadTimes(scriptThread, &tmp, &tmp, (LPFILETIME)&sk.u, (LPFILETIME)&su.u);
+
+ // Wait on the auto reset event. The event will be signalled in one of two cases:
+ // 1. When the timeout value equal to tripwireThreshold elapses, or
+ // 2. When the event is explicitly signalled from resetTripwire.
+ // A tripwireThreshold value of 0 indicates the tripwire mechanism is turned off, in which case
+ // an inifite wait is initiated on the event (which will only be terminated with an explicit signal
+ // during subsequent call to resetThreashold).
+
+ if (WAIT_TIMEOUT == WaitForSingleObject(event, 0 == tripwireThreshold ? INFINITE : tripwireThreshold))
+ {
+ // If the wait result on the event is WAIT_TIMEOUT, it means neither clearThreshold or resetThreshold
+ // were called in the tripwireThreshold period since the last call to resetThreshold. This indicates
+ // a possibility that the node.js thread is blocked.
+
+ // If tripwireThreshold is 0 at this point, however, it means a call to clearTripwire was made
+ // since the last call to resetThreshold. In this case we just skip tripwire enforcement and
+ // proceed to wait for a subsequent event.
+
+ if (0 < tripwireThreshold)
+ {
+ // Take a snapshot of the current kernel and user mode CPU utilization time of the node.js thread
+ // to determine if the elapsed CPU utilization time exceeded the preconfigured tripwireThreshold.
+ // Despite the fact this code only ever executes after the auto reset event has already timeout out
+ // after the tripwireThreshold amount of time without hearing from the node.js thread, it need not
+ // necessarily mean that the node.js thread exceeded that execution time threshold. It might not
+ // have been running at all in that period, subject to OS scheduling.
+
+ GetThreadTimes(scriptThread, &tmp, &tmp, (LPFILETIME)&ek.u, (LPFILETIME)&eu.u);
+ ULONGLONG elapsed100Ns = ek.QuadPart - sk.QuadPart + eu.QuadPart - su.QuadPart;
+
+ // Thread execution times are reported in 100ns units. Convert to milliseconds.
+
+ DWORD elapsedMs = elapsed100Ns / 10000;
+
+ // If the actual CPU execution time of the node.js thread exceeded the threshold, terminate
+ // the V8 process. Otherwise wait again while maintaining the current snapshot of the initial
+ // time utilization. This mechanism results in termination of a runaway thread some time in the
+ // (tripwireThreshold, 2 * tripwireThreshold) range of CPU utilization.
+
+ if (elapsedMs >= tripwireThreshold)
+ V8::TerminateExecution();
+ else
+ skipTimeCapture = TRUE;
+ }
+ }
+ }
+}
+
+Handle<Value> resetTripwire(const Arguments& args)
+{
+ HandleScope scope;
+
+ if (1 != args.Length() || !args[0]->IsUint32())
+ return ThrowException(Exception::Error(String::New(
+ "First agument must be an integer time threshold in milliseconds.")));
+
+ if (0 == args[0]->ToUint32()->Value())
+ return ThrowException(Exception::Error(String::New(
+ "The time threshold for blocking operations must be greater than 0.")));
+
+ tripwireThreshold = args[0]->ToUint32()->Value();
+
+ if (NULL == tripwireThread)
+ {
+ // This is the first call to resetTripwire. Perform lazy initialization.
+
+ // Create the auto reset event that will be used for signalling future changes
+ // of the tripwireThreshold value to the worker thread.
+
+ if (NULL == (event = CreateEvent(NULL, FALSE, FALSE, NULL)))
+ return ThrowException(Exception::Error(String::New("Unable to create waitable event.")));
+
+ // Capture the current thread handle as the thread on which node.js executes user code. The
+ // worker process measures the CPU utilization of this thread to determine if the execution time
+ // threshold has been exceeded.
+
+ if (!DuplicateHandle(
+ GetCurrentProcess(),
+ GetCurrentThread(),
+ GetCurrentProcess(),
+ &scriptThread,
+ 0,
+ FALSE,
+ DUPLICATE_SAME_ACCESS))
+ {
+ CloseHandle(event);
+ event = NULL;
+ return ThrowException(Exception::Error(String::New("Unable to duplicate handle of the script thread.")));
+ }
+
+ // Create the worker thread.
+
+ if (NULL == (tripwireThread = (HANDLE)_beginthread(tripwireWorker, 4096, NULL)))
+ {
+ CloseHandle(event);
+ event = NULL;
+ CloseHandle(scriptThread);
+ scriptThread = 0;
+ return ThrowException(Exception::Error(String::New("Unable to initialize a tripwire thread.")));
+ }
+ }
+ else
+ {
+ // Signal the already existing worker process using the auto reset event.
+ // This will cause the worker process to
+ // reset the elapsed time timer and pick up the new tripwireThreshold value.
+
+ SetEvent(event);
+ }
+
+ return Undefined();
+}
+
+Handle<Value> clearTripwire(const Arguments& args)
+{
+ HandleScope scope;
+
+ // Seting tripwireThreshold to 0 indicates to the worker process that
+ // there is no threshold to enforce. The worker process will make this determination
+ // next time it is signalled, there is no need to force an extra context switch here
+ // by explicit signalling.
+
+ tripwireThreshold = 0;
+
+ return Undefined();
+}
+
+void init(Handle<Object> target)
+{
+ tripwireThread = scriptThread = event = NULL;
+ NODE_SET_METHOD(target, "resetTripwire", resetTripwire);
+ NODE_SET_METHOD(target, "clearTripwire", clearTripwire);
+}
+
+NODE_MODULE(tripwire, init);
View
@@ -0,0 +1,28 @@
+// Breaks out from the infinite loop after 2 seconds
+
+var tripwire = require('../lib/tripwire.js');
+
+process.on('uncaughtException', function (e) {
+ // This code will execute so you can do some logging.
+ // Note that untrusted code should be prevented from registring to this event.
+ console.log('In uncaughtException event.');
+ process.exit(1);
+});
+
+// Terminate the process if the even loop is not responding within 2-4 seconds
+tripwire.resetTripwire(2000);
+
+try {
+ // Without tripwire, the following line of code would block the node.js
+ // event loop forever.
+ while(true);
+}
+catch (e) {
+ // This code will never execute. The only place this exception can be called is
+ // in uncaughtException handler.
+ console.log('Caught exception.');
+}
+
+// This line would normally clear the tripwire, but it will never be reached due to
+// the infinite loop that preceeds it.
+tripwire.clearTripwire();
@@ -0,0 +1,29 @@
+// Sets up a pepetual detection of blocked event loop
+
+var tripwire = require('../lib/tripwire.js');
+
+var untrustedCode = function (behaveEvil) {
+ if (behaveEvil) {
+ console.log('[Malcolm] Ha ha ha! I am taking over your event loop, Albert. Ha ha ha!');
+ while(true);
+ }
+};
+
+process.on('uncaughtException', function (e) {
+ // This code will execute so you can do some logging.
+ // Note that untrusted code should be prevented from registring to this event.
+ console.log('[Albert] Sorry Malcolm, but you overstayed your welcome.');
+ process.exit(1);
+});
+
+// Perpetually reset the tripwire every 1 second using setInterval.
+// This will terminate the process if the event loop is stopped for longer than 2 seconds
+// because the setInterval callbacks will no longer be executing.
+tripwire.resetTripwire(2000);
+setInterval(function () {
+ resetTripwire.resetTripwire(2000);
+}, 1000);
+
+// Now go execute any amount of untusted callbacks; one of them is evil
+for (var i = 0; i < 1000; i++)
+ untrustedCode(i === 666);
View
@@ -0,0 +1,25 @@
+// Time boxes execution of a piece of untrusted code to 2 seconds
+
+var tripwire = require('../lib/tripwire.js');
+
+var untrustedCode = function () {
+ console.log('[Malcolm] Ha ha ha! I am taking over your event loop, Albert. Ha ha ha!');
+ while(true);
+};
+
+process.on('uncaughtException', function (e) {
+ // This code will execute so you can do some logging.
+ // Note that untrusted code should be prevented from registring to this event.
+ console.log('[Albert] Sorry Malcolm, but you overstayed your welcome.');
+ process.exit(1);
+});
+
+// Terminate the process if the even loop is not responding within 2-4 seconds
+tripwire.resetTripwire(2000);
+
+// Now execute some untusted code
+untrustedCode();
+
+// This line would normally clear the tripwire, but it will never be reached due to
+// the runaway code that proceeds it
+tripwire.clearTripwire();

0 comments on commit 0040e7f

Please sign in to comment.