性能文章>【全网首发】Java反射机制清空字符串导致业务异常分析>

【全网首发】Java反射机制清空字符串导致业务异常分析原创

251603

编者按笔者在处理业务线问题时遇到接口返回的内容和实际内容不一致的现象。根因是业务方通过Java反射机制将String类型敏感数据引用的value数组元素全部设置为'0',从而实现清空用户敏感数据的功能。这种清空用户敏感数据的方法会将字符串常量池相应地址的内容修改,进而导致所有指向该地址的引用的内容和实际值不一致的现象。

背景知识

JVM为了提高性能和减少内存开销,在实例化字符串常量时进行了优化。JVM在Java堆上开辟了一个字符串常量池空间(StringTable),JVM通过ldc指令加载字符串常量时会调用 StringTable::intern 函数将字符串加入到字符串常量池中。

  • StringTable::intern函数代码
    oop StringTable::intern(Handle string_or_null, jchar* name,
                            int len, TRAPS)
     
    {
      unsigned int hashValue = hash_string(name, len);
      int index = the_table()->hash_to_index(hashValue);
      oop found_string = the_table()->lookup(index, name, len, hashValue);

      // Found
      if (found_string != NULL) {
        ensure_string_alive(found_string);
        return found_string;
      }

      debug_only(StableMemoryChecker **c(name, len * sizeof(name[0])));
      assert(!Universe::heap()->is_in_reserved(name),
             "proposed name of symbol must be stable");

      Handle string;
      // try to reuse the string if possible
      if (!string_or_null.is_null()) {
        string = string_or_null;
      } else {
        string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
      }

    #if INCLUDE_ALL_GCS
      if (G1StringDedup::is_enabled()) {
        // Deduplicate the string before it is interned. Note that we should never
        // deduplicate a string after it has been interned. Doing so will counteract
        // compiler optimizations done on e.g. interned string literals.
        G1StringDedup::deduplicate(string());
      }
    #endif

      // Grab the StringTable_lock before getting the_table() because it could
      // change at safepoint.
      oop added_or_found;
      {
        MutexLocker ml(StringTable_lock, THREAD);
        // Otherwise, add to symbol to table
        added_or_found = the_table()->basic_add(index, string, name, len,
                                      hashValue, CHECK_NULL);
      }

      ensure_string_alive(added_or_found);

      return added_or_found;
    }

  • StringTable::intern 函数处理流程

     

  • 字符串的创建方式

    根据StringTable::intern函数处理流程,我们可以简单描绘如下6种常见的字符串的创建方式以及引用关系。

 

现象

某业务线使用fastjson实现Java对象序列化功能,低概率出现接口返回的JSON数据的某个属性值和实际值不一致的现象。正确的属性值应该为"null",实际属性值却为"0000"。

原因分析

为了排除fastjson自身的嫌疑,我们将其替换jackson后,依然会低概率出现同样的现象。由于两个不同三方件同时存在这个问题的可能性不大,为此我们暂时排除fastjson引入该问题的可能性。为了找到该问题的根因,我们在环境中开启远程调试功能。待问题复现,调试代码时我们发现只要是指向"null"的引用,显示的内容全部变成"0000",由此我们初步怀疑字符串常量池中的"null"被修改成"0000"。

一般导致常量池被修改有两种可能性:

  1. 第三方动态库引入的bug导致字符串常量池内容被修改;
  2. 在业务代码中通过Java反射机制主动修改字符串常量池内容;

业务方排查项目中使用到的第三方动态库,未发现可疑的动态库,排除第一种可能性。排查业务代码中使用到Java反射的功能,发现清空密码功能会使用到Java反射机制,并且将String类型密码的value数组元素全部设置为'0'。

业务出现的现象可以简单通过代码模拟:

  1. 在TestString对象类中定义一个nullStr属性,初始值为"null";
  2. 定义一个带有password属性的User类;
  3. 在main方法中创建一个密码为"null"的User对象,使用Java反射机制将密码字符串的所有字符全部修改为'0',分别在密码修改前后打印TestString对象nullStr属性值;

复现代码

import java.lang.reflect.Field;
import java.util.Arrays;

public class TestString {
    private String nullStr = "null";

    public String getNullStr() {
        return nullStr;
    }

    static class User {
        private final String password;

        User(String password) {
            this.password = password;
        }

        public String getPassword() {
            return password;
        }
    }

    private static void clearPassword(User user) throws Exception {
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true);
        char[] chars = (char[]) field.get(user.getPassword());
        Arrays.fill(chars, '0');
    }

    public static void main(String[] args) throws Exception {
        User user = new User("null");
        TestString testString = new TestString();
        System.out.println("before clear password >>>>");
        System.out.println("     User.password:" + user.getPassword());
        System.out.println("TestString.nullStr:" + testString.getNullStr());
        System.out.println("--------------------------------");
        clearPassword(user);
        System.out.println("after clear password >>>>");
        System.out.println("     User.password:" + user.getPassword());
        System.out.println("TestString.nullStr:" + testString.getNullStr());
    }
}

复现代码字符串引用关系如下图所示。

User对象的password属性和TestString的nullStr属性引用都同时指向常量池中的"null"字符串,"null"字符串的value指向 {'n','u','l','l'} char数组。使用Java反射机制将User对象的password属性引用的value数组全部设置为'0',导致TestString的nullStr属性值也变成了 "0000"。

输出结果如下:

  before clear password >>>>
       User.password:null
  TestString.nullStr:null
  --------------------------------
  after clear password >>>>
       User.password:0000
  TestString.nullStr:0000

通过输出结果我们可以发现在通过Java反射机制修改某一个字符串内容后,所有指向原字符串的引用的内容全部变成修改后的内容。

总结

在保存业务敏感数据时避免使用String类型保存,建议使用byte[]或char[]数组保存,然后通过Java反射机制清空敏感数据。

后记

如果遇到相关技术问题(包括不限于毕昇 JDK),可以通过 Compiler SIG 求助。Compiler SIG 每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM 和 JDK 等相关编译技术,感兴趣的同学可以添加如下微信小助手入群。

💥看到这里的你,如果对于我写的内容很感兴趣,有任何疑问,欢迎在下面留言📥,会第一次时间给大家解答,谢谢!


原文链接:https://mp.weixin.qq.com/s/jLkof6Z1MMzBJ7eODV0pWQ

 

 

 

请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
JAVA应用生产问题排查步骤
JAVA应用生产问题排查步骤 学会这篇文章里面的命令并熟练使用,出去面试就可以说自己有5年工作经验并且精通JVM了。本篇文章中介绍的命令绝对是JAVA程序员平时工作中经常使用的并且必须会的命令,如果你
Java反射及性能
现如今的java工程中,反射的使用无处无在。无论是设计模式中的代理模式,还是红透半边天的Spring框架中的IOC,AOP等等,都存在大量反射的影子。我们今天不探讨框架层面的内容,暂且认为90%的框架
JVM是如何实现反射的:从源码解析到反射的实例演示
简介Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。在 Java 环境中运行时,对于任意一个类,能否知道这个类有哪些属性和方法
【全网首发】Java反射机制清空字符串导致业务异常分析
笔者在处理业务线问题时遇到接口返回的内容和实际内容不一致的现象,根因是Java反射机制清空字符串导致业务异常。
腾讯云和阿里云tcp三次握手的区别
前言近日同事遇到一个诡异的问题,帮忙进行了排查,好家伙不查不知道,一查让我知道了,腾讯云和阿里云TCP三次握手居然还有差异,没有想到云厂商这种Iass级别的服务,还有不同的标准~问题现象•客户是半托管客户,我们部署服务请求阿里云的nginx,nginx作为LB,反向代理了N个java
【全网首发】揭密Java常用性能调优工具的底层实现原理
当Java虚拟机出现故障和性能问题时,我们通常会借助一些业界知名的工具来辅助排查问题。为了能更好的利用这些工具,我们通常需要对这些工具的实现原理有所了解,现有资料在介绍一些性能排查和故障诊断工具时,通常只会围绕这个工具的实现原理展开,例如Eclipse的MAT插件,主要是解读虚拟机的Dump文件。这