delight-nashorn-sandbox JsSanitizer 第2环节,注入函数调用时,使用正则匹配可能导致死循环,进而线程CPU飙升

探讨 JsSanitizer 在特定 JS 脚本处理时出现的死循环问题及解决策略,涉及 JDK 8 的正则表达式缺陷。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

环境:

java version "1.8.0_271"

1. 现象

JsSanitizer.java

	/**
	 * The name of the variable which holds reference to interruption checking
	 * class. To prevent collisions random suffix is added.
	 */
	final static String JS_INTERRUPTED_TEST = "__it";

	private final static List<PoisonPil> POISON_PILLS = Arrays.asList(
			// every 10th statements ended with semicolon put interrupt checking function
			new PoisonPil(Pattern.compile("(([^;]+;){9}[^;]+(?<!break|continue);\\n(?![\\W]*(\\/\\/.+[\\W]+)*else))"),
					JS_INTERRUPTED_FUNCTION + "();\n"),
			// every (except switch) block start brace put interrupt checking function
			new PoisonPil(Pattern.compile("(\\s*for\\s*\\([^\\{]+\\)\\s*\\{)"), JS_INTERRUPTED_FUNCTION + "();"), // for
																													// with
																													// block
			new PoisonPil(Pattern.compile("(\\s*for\\s*\\([^\\{]+\\)\\s*[^\\{]+;)"), JS_INTERRUPTED_FUNCTION + "();"), // for
																														// without
																						// block
			//
			new PoisonPil(Pattern.compile("(\\s*([^\"]?function)\\s*[^\"}]*\\([\\s]*\\)\\s*\\{)"),
					JS_INTERRUPTED_FUNCTION + "();"), // function except when enclosed in quotes
			new PoisonPil(Pattern.compile("(\\s*while\\s*\\([^\\{]+\\{)"), JS_INTERRUPTED_FUNCTION + "();"),
			new PoisonPil(Pattern.compile("(\\s*do\\s*\\{)"), JS_INTERRUPTED_FUNCTION + "();"));


	String injectInterruptionCalls(final String str) {
		String current = str;
		for (final PoisonPil pp : POISON_PILLS) {
			final StringBuffer sb = new StringBuffer();
			final Matcher matcher = pp.pattern.matcher(current);
			while (matcher.find()) {
				matcher.appendReplacement(sb, ("$1" + pp.replacement));
			}
			matcher.appendTail(sb);
			current = sb.toString();
		}
		return current;
	}
POISON_PILLS中的正则表达式,在处理特殊js脚本时,会出现死循环,线程CPU飙升:

"http-nio-8103-exec-26" #898 daemon prio=5 os_prio=0 tid=0x00007f1370068000 nid=0x18a runnable [0x00007f125f3ec000]
   java.lang.Thread.State: RUNNABLE
    at java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4274)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$Slice.match(Pattern.java:3986)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)

    ...... (循环出现)

    at delight.nashornsandbox.internal.JsSanitizer.injectInterruptionCalls(JsSanitizer.java:231)
    at delight.nashornsandbox.internal.JsSanitizer.secureJsImpl(JsSanitizer.java:279)
    at delight.nashornsandbox.internal.JsSanitizer.secureJs(JsSanitizer.java:258)
    at delight.nashornsandbox.internal.NashornSandboxImpl.eval(NashornSandboxImpl.java:221)
    at delight.nashornsandbox.internal.NashornSandboxImpl.eval(NashornSandboxImpl.java:203)

可供测试的js代码:

var PROPERTY_SET = 0x11;

function system_getString(view, position) {
    var length = view.getUint16(position);
    var strValue = '';
    for (var i = 0; i < length; i++) {
        strValue += String.fromCharCode(view.getUint8(position + 2 + i));
    }
    return strValue;
}

function system_getString(view, length, position) {
    var length1 = length
    var strValue = '';
    for (var i = 0; i < length1; i++) {
        strValue += String.fromCharCode(view.getUint8(position + i));
    }
    return strValue;
}

function decode(bytes) {
    var uint8Array = new Uint8Array(bytes.length);
    for (var i = 0; i < bytes.length; i++) {
        uint8Array[i] = bytes[i] & 0xff;
    }
    var dataView = new DataView(uint8Array.buffer, 0);
    var jsonMap = {};
    var fHead = uint8Array[0]; // command
    if (fHead == 0x01) {
        var params = {};
        var data = [];

        var property = {};
        property['key'] = 'testAdd';
        property['value'] = system_getString(dataView, 7, 1); //test
        //property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
        data.push(property);

        property = {};
        property['key'] = 'testReading';
        property['value'] = dataView.getInt32(15).toString(); //
        //property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
        data.push(property);

// 		 property = {};
//         property['key'] = 'test';
//         property['value'] = dataView.getInt8(19).toString(); //
//         //property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
//         data.push(property);

// 		 property = {};
//         property['key'] = 'test';
//         property['value'] = dataView.getInt8(20).toString(); //
//         //property['ts'] = system_getBigUint64(dataView, 21).toString(); //test
//         data.push(property);

        params['data'] = data;
        jsonMap['params'] = params; //json
    }
    return JSON.stringify(jsonMap);
}

function encode(jsonObj) {
    var bytes = [];
    return bytes;
}

2 . 分析原因

jdk8问题,在jdk9中修复

TODO 还没使用jdk9验证

(另外,发现正则匹配耗时很长,如果在后端,需要提升TPS,可以考虑把注入函数环节去掉)

3. 优化方法

去掉注入函数环节

3.1. 在JsSanitizer的派生类中,直接返回格式化后的js脚本,不再注入函数调用即可。

    @Override
    protected String injectInterruptionCalls(final String str) {
        return str;
    }

3.2. 上述方法已经可以解决问题,另外,js脚本中不再注入检测中断的代码,那么,Java代码中,调用interrupt方法发出中断的代码,也可以优化下

在ThreadMonitor.java中,

void run() {
   try {
      // wait, for threadToMonitor to be set in JS evaluator thread
      synchronized (monitor) {
         if (threadToMonitor == null) {
            monitor.wait((maxCPUTime + 500) / MILI_TO_NANO);
         }
         if (threadToMonitor == null) {
            timedOutWaitingForThreadToMonitor = true;
            throw new IllegalStateException("Executor thread not set after " + maxCPUTime / MILI_TO_NANO + " ms");
         }
      }
      final long startCPUTime = getCPUTime();
      final long startMemory = getCurrentMemory();
      while (!stop.get()) {
         final long runtime = getCPUTime() - startCPUTime;
         final long memory = getCurrentMemory() - startMemory;
         
         if (isCpuTimeExided(runtime) || isMemoryExided(memory)) {
            
            cpuLimitExceeded.set(isCpuTimeExided(runtime));
            memoryLimitExceeded.set(isMemoryExided(memory));
            threadToMonitor.interrupt(); // 在js中没有了注入的检测中断代码,此处在Java中调用interrupt方法,也不起作用,后面的等待50ms,也应该是无效的等待,降低解析效率
            synchronized (monitor) {
               monitor.wait(50);
            }
            if (stop.get()) {
               return;
            }
            if (!scriptFinished.get()) {
               LOG.error(this.getClass().getSimpleName() + ": Thread hard shutdown!");
               threadToMonitor.stop();// 可以考虑直接调用stop方法(虽然stop是deprecated)
               scriptKilled.set(true);
            }
            return;
         } else {
            
         }
         synchronized (monitor) {
            long waitTime = getCheckInterval(runtime);
            
            if (waitTime == 0) {
               waitTime = 1;
            }
            monitor.wait(waitTime);
         }
         
      }
   } catch (final Exception e) {
      throw new RuntimeException(e);
   }
}

TODO Java中调用interrupt,在执行js代码时没有捕获到该异常,可能要看下nashorn源码了吧

参考:

https://blog.csdn.net/yhtppp/article/details/118785609  接上一篇

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6988218     JDK-6988218 : RegEx matcher loops ,在jdk9中修复

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=5050507     JDK-5050507 : Pattern.matches throws StackOverFlow Error

https://blog.csdn.net/shixing_11/article/details/5997567

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值