読者です 読者をやめる 読者になる 読者になる

sun.misc.UnsafeでFinalなオブジェクトでも書き換える

Finalなフィールドはリフレクションを使っても書き換えられませんよね。
強引に書き換えようとしてもIllegalAccessExceptionなりになってしまい書き換えられません。

基本となるフィールドが final の場合、メソッドは IllegalAccessException をスローします。

http://sdc.sun.co.jp/java/docs/j2se/1.4/ja/docs/ja/api/java/lang/reflect/Field.html#set%28java.lang.Object,%20java.lang.Object%29

でもなぜかシリアライズしてデシリアライズすると書き換えられる。

これはなんでじゃ?ってことでjava.io.ObjectInputStream調べてみるとsun.misc.Unsafeを使用しているのがわかる。
このクラスを眺めていると面白いことにコンストラクタを呼ばないでインスタンスを作成することができる。
つまりこれで書いたメモリの確保だけしているけどコンストラクタが完了していない状態を作り出すことができる。
そしてつまりコンストラクタの中で定義されるFinalな定数をリフレクションで書き換えることができてしまう。
※Finalは定数というより「一度設定すると変更不可な変数」という扱いの為。初期値を代入しない場合一度は代入することが可能。
こんな風にしてシリアライズシリアライズは実現されているんですね。

たとえばこんな感じで書き換えが可能。

package jp.co.gara.unsafe;
import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class UnsafeTest {

   public static void main(String[] args) throws Exception {
       new UnsafeTest().go();
   }
   private void go() throws Exception {
       rewrite();
       unsafeRewrite();
   }
   private void rewrite() throws Exception {
       System.out.println("リフレクションでFinalを書き換えトライ");
       Target tar = new Target();
       Field finalField = Target.class.getDeclaredField("sex");
       finalField.setAccessible(true);
       finalField.set(tar,"性転換"); //AccessibleをTrueにしているのでエラーはない
       tar.print();
       System.out.println("もちろん無理。setAccessibleしているのでエラーは出ない。");
   }
   private void unsafeRewrite() throws Exception {
       System.out.println("UnSafeでFinalを書き換えトライ");
       Field field = Unsafe.class.getDeclaredField("theUnsafe");
       field.setAccessible(true);
       Unsafe unsafe =(Unsafe) field.get(null);
       Target tar = (Target) unsafe.allocateInstance(Target.class);
       unsafe.putObject(tar, unsafe.objectFieldOffset(Target.class.getDeclaredField("sex")), "強制性転換!");
       tar.print();
       System.out.println("なんと書き換えられる。さっすが!");
   }
}

class Target{
   private String name = "";
   private int age = 0;
//    private final String sex = "MAN"; //こう書かれて初期化されるとUnSafeでも更新は無理。
   private final String sex;
   public Target(){
       name = "GARAPON";
       age = 14;
       sex = "MAN";
   }
   public void print() {
       System.out.println("name = [" + name + "] age = [" + age + "] sex =[" +sex + "]");
   }
}

実行結果

リフレクションでFinalを書き換えトライ
name = [GARAPON] age = [14] sex =[性転換]
もちろん無理。setAccessibleしているのでエラーは出ない。
UnSafeでFinalを書き換えトライ
name = [null] age = [0] sex =[強制性転換!]
なんと書き換えられる。さっすが!

面白いですね。
それにしてもsun.misc.Unsafeは「allocateMemory」だとか「reallocateMemory」だとか「allocateInstance」だとかおもしろそうなメソッドがたくさんあって遊びがいがありますね。

参考

俺様デシリアライザを作ろうとして困ったこと « 来栖川電算
sun.misc.Unsafe - ナンセンス不定記


とおもったらなんかこの例だとリフレクションもFinalに書き換えできてしまっている。あれ?なんでだ?
やっぱよってると駄目だわからん。

広告を非表示にする