javascriptnode.jsv8node-inspectorvows

V8 lazy generation of stack traces seems to cause an infinite loop in the vows library


I spent some time debugging a strange infinite loop problem in a NodeJS testsuite. It only happens under rare conditions but I can reproduce it when I attach to the chrome debugger.

I think it has to do with V8's handling of stack traces in exceptions and a extension that the vows library did to the AssertionError object (vows added an toString method). I could also be mistaken, so I wanted to ask whether my understanding of V8's implementation is correct.

Here is a minimal example to reproduce the error:

$ git clone https://github.com/flatiron/vows.git
$ cd vows && npm install && npm install should

$ cat > example.js
var should = require('should');
var error = require('./lib/assert/error.js');

try {
  'x'.should.be.json;
} catch (e) {
  console.log(e.toString());
}

// without debug, it should fail as expected
$ node example.js
expected 'x' to have property 'headers' // should.js:61

// now with debug
$ node-inspector &
$ node --debug-brk example.js

// 1) open http://127.0.0.1:8080/debug?port=5858 in Chrome
// 2) set breakpoint at lib/assert/error.js#79 (the toString method)
// 3) Resume script execution (F8)

Now the program ends up in an infinite loop: the toString method (added by the vows library) is called again and again when this.stack is accessed in the regexp on line 83.

require('assert').AssertionError.prototype.toString = function () {
var that = this, // line 79: breakpoint
    source;

if (this.stack) {
    source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/); // line 83: infinite loop takes place here (however, this.stack is undefined)
}

When I inspect this in the debugger, it shows that it is an AssertionError but its stack property is undefined. However, when I hover with the mouse over it, it shows the actual stack trace.

I assume this phenomena is caused by the laziness optimization of V8. It only computes the stack trace on demand. In doing so, it interferes with the added toString method of vows. The toString method again accesses the stack trace (this.stack), so the loop continues.

Is that conclusion correct? If so, is there a way to patch the vows code, so it works with V8 (or can I at least report it as a bug in the vows project)?

I'm using node v0.10.28 under Ubuntu.

Update: Simplified example without vows

Here is a simplified version. It no longer depends on vows, but instead I have only copied the relevant parts of its toString extension:

var should = require('should');

require('assert').AssertionError.prototype.toString = function () {
  var that = this,
    source;

  if (this.stack) {
    source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/);
  }

  return '<dummy-result>';
};

try {
  'x'.should.be.json;
} catch (e) {
  console.log(e.toString());
}

// expected result (without debug mode)
$ node example.js
<dummy-result>

In debug mode, the recursion takes place in the if statement.

Update: Even simplier version with ReferenceError

ReferenceError.prototype.toString = function () {
  var that = this,
    source;

  if (this.stack) {
    source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/);
  }

  return '<dummy-result>';
};

try {
  throw new ReferenceError('ABC');
} catch (e) {
  console.log(e.toString());
}

(I also create a jsfiddle example, but I cannot reproduce the infinite loop there, only with node.)


Solution

  • Congratulations, you have found a bug in V8!

    Yep, that's definitely a bug in the V8 version in that version of node.

    The code in the version of V8 your version of Node uses code that looks something like:

    function FormatStackTrace(error, frames) {
      var lines = [];
      try {
        lines.push(error.toString());
      } catch (e) {
        try {
          lines.push("<error: " + e + ">");
        } catch (ee) {
          lines.push("<error>");
        }
     }
    

    Here is the actual code from the version NodeJS uses.

    The fact it's doing error.toString() itself is causing the loop, this.stack accesses FormatStackTrace which in turn is doing .toString(). Your observation is correct.

    If that's any comfort, that code looks very different in newer versions of V8. In Node 0.11, this bug is already fixed.

    Here is the commit that fixed it commited A year and a half ago by Vyacheslav Egorov.

    What you can do about it?

    Well, your options are limited, but since this is for debugging anyway, can always prevent the second iteration and stop the loop:

    ReferenceError.prototype.toString = function () {
      var source;
      if(this.alreadyCalled) return "ReferenceError";
      this.alreadyCalled = true;
      if (this.stack) {
        source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/);
      }
    
      return '<dummy-result>';
    };
    

    Does not exhibit the same issue. There is not much more you could do without access to the core code.