长亭百川云 - 文章详情

JRASP内存泄漏检测与清除实践

RASP安全技术

57

2024-07-13

热加载与卸载已经成为RASP的标配,而涉及到插件或者脚本的卸载问题,却少有技术文档提及, 主要原因是RASP开发人员更多的偏向安全,即使是经验丰富的Java工程师,遇到内存泄露问题,也会感到棘手。

类卸载的条件十分苛刻,要同时满足下面的三个条件:

  • 类所有的实例对象已经被回收;

  • 加载该类的Classloder已经被回收;

  • 该类对应的java.lang.Class对象没有任何对方被引用;

1. Class对象引用置空

==================

一般的,无需关心class对象的引用关系,但是有些场景(如反射场景)会缓存类的Class对象,使得class对象的引用存在,导致JRASP无法卸载。

2. 类加载器置空

以JDK8为例子,Java虚拟机自带的类加载器有BootstrapClassLoader、ExtensionClassLoader和SystemClassLoader, 这些类加载器在JVM整个生命周期中都不会被置为空,因此它们加载的类也不会被卸载。

而用户自定义的类加载器,可以在使用完成之后将加载器的对象置空,从而满足类卸载的三个条件之一。

以JRASP代码为例子,执行卸载关闭操作之后,将自定义类加载器置空。

如果自定义类加载器没有正确的置空,JRASP将不会被完全的清理,从而引发内存泄漏。 现在我们做一个测试,将上面代码截图的第90行的 raspClassLoader=null;注释掉(即类加载器不置空)。

打包编译后加载JRASP后再执行卸载操作,主动Full GC jmap-histo:live50730|grep com.jrasp.agent.core,结果如下所示:

 `206:            50           3200  com.jrasp.agent.core.log.LogRecord` `307:            12           1344  com.jrasp.agent.core.classloader.ModuleJarClassLoader` `460:            12            480  com.jrasp.agent.core.module.CoreModule` `542:            12            288  [Lcom.jrasp.agent.core.classloader.RoutingURLClassLoader$Routing;` `548:            12            288  com.jrasp.agent.core.classloader.RoutingURLClassLoader$Routing` `// 其他类省略...`

在Full GC之后,JRASP实例个数不为空,存在内存泄漏。使用性能诊断(jprofile,eclipse的MAT工具也是可以)工具,查看JRASP的对象:

查看其中一个对象的引用关系,如下所示:

从上图的引用关系可以明显看出,存在一条引用链路,链路从 RaspClassloader开始指向 LogRecord (上图中两个红色圈的之间的灰色线) (黄色为class对象,红色为GC Roots)

内存泄漏的原因:classloader没有置为空,导致内存泄漏

3. 对象置空和资源关闭

以JRASP为例说明,这里包括线程池关闭、自定义线程、定时器停止、shutdownHook移除、ClassFileTransformer移除和threadlocal线程变量清除等.

3.1 定时器停止

完全停止 java.lang.timer,需要将定时器线程停止,并将任务执行队列清空。

3.2 shutdownHook移除

在Java进程关闭之前,能够即时的清理rasp占用的磁盘等资源,shutdownHook可以执行指定的操作。

如果主动关闭rasp,没有清理shutdownHook,将会导致内存泄漏。

3.3 线程池关闭

如果RASP使用到了线程池,在卸载时需要关闭。即使关闭了线程池,由于jvm线程池重写了 finalize方法,一次FullGC依然无法清除残留的对象。 JRASP 1.2.x(商业版本) 已经把线程池替换为多个 java.lang.timer,卸载时非常清爽干净。

3.4 线程变量的清除

在JRASP中,使用线程变量threadlocal关联请求上下文与具体的hook类,来辅助检测功能。

在RASP卸载时,需要将线程thread中缓存的threadlocal对象。

在介绍JRASP实现方案之前,先来看下tomcat是如何实现热卸载的和内存泄漏检测的。

3.4.1 tomcat资源清除与内存泄漏检测

tomcat在卸载war包时,调用war的类加载器 WebappClassLoaderBase对象的stop方法完成资源的关闭与清理操作。

具体的引用清除实现来在 clearReferences中,主要有:注销JDBC驱动、关闭应用创建的线程、检查线程变量的内存泄漏等,关闭连接和线程的操作容易实现,本节主要针对线程变量的内存泄漏清理与检测。

  • 线程变量泄漏检测

在线程Thread对象中使用两个字段保存该线程使用的 threadlocal对象:



1.  `public class Thread implements Runnable {`
    

3.      `/* ThreadLocal values pertaining to this thread. This map is maintained`
    
4.       `* by the ThreadLocal class. */`
    
5.      `ThreadLocal.ThreadLocalMap threadLocals = null;`
    

7.      `/*`
    
8.       `* InheritableThreadLocal values pertaining to this thread. This map is`
    
9.       `* maintained by the InheritableThreadLocal class.`
    
10.       `*/`
    
11.      `ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;`
    

13.      `// 其他代码省略...`
    
14.  `}`   
    


threadLocals的类型是 ThreadLocalMap,ThreadLocalMap中用数组table保存threadlocal变量的key、value。 因此最终我们需要清理的是这个table里面的Entry。



1.  `static class ThreadLocalMap {`
    

3.      `static class Entry extends WeakReference<ThreadLocal<?>> {`
    
4.          `Object value;`
    
5.          `Entry(ThreadLocal<?> k, Object v) {`
    
6.              `super(k);`
    
7.              `value = v;`
    
8.          `}`
    
9.      `}`
    

11.      `// 保存threadlocal变量的key、value`
    
12.      `private Entry[] table;`
    
13.  `}`    
    


tomcat中线程变量的内存泄漏检测代码在 checkThreadLocalsForLeaks中。

主要是反射threadLocals、inheritableThreadLocals

3.4.2 JRASP线程清除方案

一般的在使用完线程变量之后,要及时的调用 threadlocal.remove() 将线程变量移除。 但是对于RASP来说,业务线程池线程复用机制,并且无法确定什么时候任务执行完成,也就无法在任务执行完成之后清除。

JRASP采用类似于tomcat线程变量内存泄漏的检测方式,即反射调用 threadlocal.remove()方法。

cleanThreadLocals的实现:

上面的方案存在一些限制,JDK17以上禁止了跨模块的反射,上面的反射调用执行会报错,需要业务在JVM参数中增加 --add-opens=java.base/java.lang=ALL-UNNAMED 解除限制。(增加参数成本较低,业务使用了三方包也会开启该参数)

4. 总结

  本文介绍了JRASP卸载时的一些坑,并给出了解决方案,特别是线程池的threadlocal内存泄漏, 给出了检测和卸载代码,该方案在JRASP上使用较为成功。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2