一次诡异的 Node.js 内存爆炸排查:从 7GB OOM 到 70MB 的根因之旅
问题背景
一个普通的 Node/Express 应用(内含若干自动化子任务),本地用 Node 跑一切正常:内存稳定在 100MB 左右,端口监听、接口响应全部正常。
但换到 Bun 运行时(或者打包成 bundle 后在内存受限的容器里跑),现象完全变了样:
-
进程 RSS 疯涨到 6~7GB,直接被容器 OOM Kill
-
换个环境又变成 CPU 100% 死循环,但内存不动、日志一行不输出
-
崩溃表现随环境漂移,没有任何一致性
排查过程:三个假设,逐一证伪
假设一:bundle 太大导致内存高?
对比测试:同一个入口,用 Node 跑 RSS 只有 106MB,用 Bun 跑 VSZ 飙到 19GB。排除打包问题——是运行时差异。
假设二:Bun + Express 本身有 bug?
写了个 10 行的最小 Express 服务用 Bun 跑:内存 58MB,稳如老狗。排除通用性问题——是这份代码特有的。
假设三:混淆代码的反调试逻辑不兼容 JSC?
这份代码经过 JavaScript Obfuscator 混淆,里面藏着几类典型的「防篡改」机关。V8(Node)和 JavaScriptCore(Bun)对它们的行为差异,就是问题的根源:
-
自毁死循环:字符串解密器里有个循环不断往数组 push 随机数再重置边界,在某些求值路径下永远不收敛——Node 的优化路径恰好跳过了它,Bun 没有
-
校验和旋转循环:
while(true)里用parseInt拼出一串魔法数求和,和目标值相等才 break。依赖浮点取整的微妙行为,JSC 下永远算不出目标值 → 100% CPU 空转 -
Function('return this')全局对象探测:沙箱环境拦截后会 fallback 到window,Node 里window未定义直接 ReferenceError,反而掩盖了真实错误
解决方案:不是对抗,是绕开
逐行硬啃混淆代码效率太低,最终用 webcrack 一键反混淆:
npx webcrack index.js -o ./deob
95575 字节的混淆代码 → 595 行可读的干净代码。所有反调试机关被剥除,字符串还原成明文,函数名恢复语义。反混淆后的代码在 Bun 下运行:内存 71MB,一切功能正常。
复盘:三条可复用的经验
-
「换个运行时就崩」先查运行时差异,别急着怀疑自己的代码。V8 和 JSC 在边界行为(整数溢出、浮点取整、原型链探测)上的差异,会让混淆/依赖 hack 的代码表现完全不同。
-
最小化隔离测试是定位利器。「官方运行时 + 最小 Express」跑通,立刻把问题范围从「平台/依赖」缩到「这份代码」;再二分注释掉可疑代码段,几轮就锁定了混淆 prelude。
-
对混淆代码,反混淆优先于打补丁。我在反混淆前尝试了固化字符串表、逐个删除死循环等手动补丁,每次修好一个崩溃点就冒出下一个(语法坏了、递归栈溢出、undefined 调用链……),折腾近十轮。webcrack 30 秒解决战斗——工具存在就先用工具。
- 点赞
- 收藏
- 关注作者
评论(0)