V8惰性生成堆栈跟踪似乎导致vows库中出现无限循环的问题。

11

我花了一些时间在 NodeJS 的测试套件中调试一个奇怪的无限循环问题。它只在罕见的情况下发生,但当我连接到 Chrome 调试器时,我可以重现它。

我认为这与 V8 对异常中的堆栈跟踪以及堆栈跟踪API有关,并且是 vows 库AssertionError 对象所做的扩展(vows 添加了一个 toString 方法)。但我可能错了,所以我想问问我的 V8 实现是否正确。

下面是一个最小化的示例来重现此错误:

$ 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)

现在这个程序陷入了一个无限循环中:当在第83行的正则表达式中访问this.stack时,toString方法(由vows库添加)会一遍又一遍地被调用。

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)
}
当我在调试器中检查this时,它显示为AssertionError,但它的stack属性是undefined。然而,当我用鼠标悬停在上面时,它显示实际的堆栈跟踪。
我认为这种现象是由V8的惰性优化引起的。它只在需要时计算堆栈跟踪。这样做会干扰vows的toString方法的添加。toString方法再次访问堆栈跟踪(this.stack),所以循环继续进行。
那个结论正确吗?如果是,是否有一种方法可以修补vows代码,使其能够在V8下工作(或者我至少可以将其报告为vows项目中的错误)?
我正在Ubuntu下使用node v0.10.28。
更新:没有vows的简化示例
这是一个简化版本。它不再依赖于vows,而是仅复制了其toString扩展的相关部分。
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>

在调试模式下,递归发生在 if 语句中。

更新:使用 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());
}

我还创建了一个jsfiddle示例,但是我在那里无法复制无限循环的情况,只有在node中才能实现。


1
你能否创建一个不使用vows的自包含示例? - Benjamin Gruenbaum
@BenjaminGruenbaum 是的,我能够通过使用它的toString方法来重现相同的行为。我还删除了非相关部分,只剩下递归。 - Philipp Claßen
1
当我修改ReferenceError时,异常会被抛出,并且不会有无限循环的情况发生。AssertionError是由“should”引发的异常。我将尝试通过实际的抛出来进一步简化这个例子。 - Philipp Claßen
1
@BenjaminGruenbaum 好的,现在我明白你的意思了。用一个简化的ReferenceError示例来重现它非常简单。我会更新我的帖子。 - Philipp Claßen
1
非常抱歉,使用节点调试器可以很好地重现此问题。我忘记了步入 -_-。 - Benjamin Gruenbaum
显示剩余2条评论
1个回答

7

恭喜,您在V8中发现了一个漏洞!

是的,在那个版本的Node中,V8中肯定存在一个漏洞。

您的Node版本使用的V8版本中的代码看起来类似于:

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

以下是关于NodeJS使用版本的实际代码。
它自己执行error.toString()导致了循环,this.stack访问FormatStackTrace,后者又执行了.toString()。你的观察是正确的。
如果这能让你感到安慰,那么在更新版本的V8中,该代码看起来非常不同。在Node 0.11中,这个错误已经被修复
以下是修复该错误的提交,由Vyacheslav Egorov在一年半前提交。

你可以怎么做?

好的,你的选择有限,但既然这只是为了调试,你总可以防止第二次迭代并停止循环:

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>';
};

没有展示相同的问题。如果没有核心代码的访问权限,您就无法做更多事情。


太好了。目前我无法访问不稳定节点,因此我无法对其进行0.11版本的测试(也许稍后我可以)。您认为我是否应该提交错误报告,以便可以将此补丁回溯到稳定节点0.10.x吗? - Philipp Claßen
1
我非常怀疑他们会回溯更新,因为它对他们的影响不大,而且 vows 本身已经有9个月没有更新了。我认为一个肮脏的解决方法是自己覆盖 .toString() 方法,并使用一个布尔型的“已调用”标志。顺便说一句,你发现得真好。 - Benjamin Gruenbaum
1
我只是为了以防万一在Node中打开了一个问题 :) - Benjamin Gruenbaum
谢谢,我还在vows中开了一个问题,作为一个警告,如果你同时使用vows(或easy-api)和“should”:https://github.com/flatiron/vows/issues/303。 - Philipp Claßen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接