Peaceful and Bright Future of Integrity by Default in Java

原文はこちら。
The original article was written by Ana-Maria Mihalceanu (Senior Developer Advocate, Oracle).
https://inside.java/2025/01/03/evolving-default-integrity/

Javaが進化し続けるにつれ、その安定性と堅牢性への取り組みはより強固なものとなっています。最近のJDKリリースや今後リリースされるJDKでは、この傾向が反映されており、安全でないAPIの境界が徐々に拡張されています。具体的には、動的エージェントのロードの制限、潜在的に安全でないメモリアクセス方法の廃止、Java Native Interface(JNI)の使用に関するより厳格な規定の準備などです。JDK 22で導入されたForeign Function and Memory (FFM) APIは、JVM外のコードの呼び出し(外部関数)を簡素化する一方で、JVMが管理していないメモリへのアクセス(外部メモリ)に対しては、より安全なアプローチを提供しています。これらの変更は、Javaの回復力を維持し、integrity by default(デフォルトで安全)を重視する傾向をさらに反映しています。 本記事では、これらの変更が、より予測可能で信頼性の高いJavaエコシステムへの賢明な移行をどのように体現しているかを考察します。

JEP 454: Foreign Function & Memory API
https://openjdk.org/jeps/454

Integrity in the Java Platform

ソフトウェアにおいて、Integrity(整合性、完全性、安全性)とは、プログラムを構築する構造が完全かつ健全であることを意味します。つまり、Javaプラットフォームの仕様は網羅的(完全性)であり、その実装は仕様を厳格に遵守している(健全性)ということです。この基盤となるintegrityにより、Javaプラットフォームの構造を基盤として、自信を持ってアプリケーションロジックを構築できます。

Integrity by DefaultというJEP(ドラフト)では、望ましくない、あるいは不適切な使用からコードやデータを保護することの重要性を強調しています。

JEP draft: Integrity by Default
https://openjdk.org/jeps/8305968

integrityを確立するための基本的なツールはカプセル化ですが、Javaプラットフォームにはいくつかの安全でないAPIが含まれています。そのAPIはこのintegrityに対する期待を裏切る可能性があり、お客様のアプリケーションやライブラリの正確性、保守性、拡張性、セキュリティ、パフォーマンスに影響を及ぼす可能性があります。

Instrumentation APIこのAPIを使うと、エージェントは任意のクラスの任意のメソッドのバイトコードを変更できます。https://docs.oracle.com/en/java/javase/23/docs/api/java.instrument/java/lang/instrument/package-summary.html
AccessibleObject::setAccessible(boolean) メソッドこのメソッドを使うと、カプセル化の境界に関係なく、フィールドとメソッドのリフレクションを可能にします。このメソッドの目的はオブジェクトのシリアライゼーションとデシリアライゼーションのサポートですが、アクセス権限があれば、任意のコードで任意のクラスのプライベートメソッドを呼び出したり、任意のオブジェクトのプライベートフィールドを読み書きしたり、さらにはfinalフィールドの書き込みさえも可能です。https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/reflect/AccessibleObject.html#setAccessible(boolean)
sun.misc.Unsafeクラスこのクラスには、カプセル化の境界を無視して、privateメソッドやフィールドへのアクセス、finalフィールドへの書き込みが可能なメソッドが含まれています。
Java Native Interface (JNI)JNIにより、ネイティブコードがJavaオブジェクトとやりとりできるようになります。ネイティブコードは、カプセル化の境界を無視してprivateメソッドやフィールドにアクセスし、finalフィールドを書き込みできます。https://docs.oracle.com/en/java/javase/23/docs/specs/jni/index.html

これらの安全でないAPIの誤用を防ぐため、提案では、ライブラリ、フレームワーク、ツールがデフォルトで利用できないように、こうしたAPIへの直接アクセスを制限することを推奨しています。さらに一連のJEPでは、安全でないAPIを使用する際の各問題点を段階的に解決し、移行の選択肢を提供するとともに、必要に応じてこのデフォルトの制限をオーバーライドできるようにしています。

Gradual Limitation of Access to Unsafe APIs and Alternatives

Organize to Disallow Dynamic Loading of Agents

integrityというテーマに基づいて、JDK 21の拡張(JEP 451)は、実行中のJava仮想マシン(JVM)へのエージェントの動的ロードの影響に焦点を当てました。

JEP 451: Prepare to Disallow the Dynamic Loading of Agents
https://openjdk.org/jeps/451

エージェントはJDK 5で導入されたコンポーネントで、クラスのインスツルメンテーションを可能にし、プロファイラのようなツールがアプリケーションを監視できるようにします。例えば、リモートのJavaアプリケーションをデバッグするために、-agentlib:jdwp オプションを使って、JVMに組み込まれたエージェントを起動時に有効にするように設定したことがあるかもしれません。その裏では、Javaランチャーは適切なOS権限で起動されたツールが、実行中のJVMに接続できるようにするAttach APIを使用しています。

jdk.attach (Java SE 23 & JDK 23)
https://docs.oracle.com/en/java/javase/23/docs/api/jdk.attach/module-summary.html

しかし、ある種のライブラリはAttach APIを悪用して、実行中のJVMに黙って接続し、エージェントを動的にロードし、コード変更の超能力を得ていました。その結果、エージェントを動的にロードすることは、アプリケーションのintegrityに対するリスクをもたらします。JDK21以降、エージェントが動的にロードされるとJVMは警告を発し、このようなリスクを警告しますが、同時に、このような動作がデフォルトで禁止されるかもしれない将来のリリースに備えるためでもあります。この変更は、エージェントを動的にロードする必要のないツールの大部分には影響しません。

💡ライブラリがエージェントを使用する場合は、起動時に -javaagent/-agentlib オプションでエージェントをロードするのが正統な方法です。このアプローチは、サービス性の要求とアプリケーションのintegrityを維持する必要性とのバランスをとるものです。

Act upon Unsafe Memory-Access Methods Usage

sun.misc.Unsafeクラスは、これまで開発者が低レベルの操作、特に次のようなタスクにアクセスできるようにしてきました。

  • より良いパフォーマンスを得るための直接的なメモリ操作
  • ByteBufferの制限を受けないオフヒープメモリの処理
  • compare-and-swapのようなアトミック操作の実行

Compare-and-swap
https://en.wikipedia.org/wiki/Compare-and-swap

しかしながらこれらのメソッドの利用にはリスクが伴います。

  • JVMの最適化をバイパスするため、未定義の動作やクラッシュ、パフォーマンスの悪化につながる可能性がある。
  • JVM内部の低レベルの詳細が公開され、Javaバージョン間の互換性の問題につながる。
  • その安全でない性質ゆえに、保守性とセキュリティの問題を引き起こす。

Java 23から、JDKはsun.misc.Unsafeのメモリアクセスメソッドを段階的に廃止しています。これは、これらの安全でない操作に関連するリスクと制限のためです。代わりに推奨されるのは、VarHandle API(JDK 9 で導入)と Foreign Function and Memory API(JDK 22 で導入)です。

JEP 193: Variable Handles
https://openjdk.org/jeps/193
JEP 454: Foreign Function and Memory API
https://openjdk.org/jeps/454

例えば、オンヒープ・メモリ操作の場合、アトミックな更新のためにUnsafeを使用したことがあるかもしれません。このような危険な行為を避けるために、UnsafeからVarHandleへ移行し、同じことを実現できます。

// Migration example from Unsafe
private static final Unsafe UNSAFE = ...;
private static final long OFFSET;

static {
    try {
        OFFSET = UNSAFE.objectFieldOffset(Point.class.getDeclaredField("x"));
    } catch (Exception ex) {
        throw new AssertionError(ex);
    }
}

private int x;

public boolean update(int newValue) {
    return UNSAFE.compareAndSwapInt(this, OFFSET, x, newValue);
}

/// Use VarHandle to achieve the same
private static final VarHandle HANDLE = MethodHandles.lookup().findVarHandle(Point.class, "x", int.class);

public boolean update(int newValue) {
    return HANDLE.compareAndSet(this, x, newValue);
}

同様に、オフヒープメモリ操作にUnsafeを使用していた場合、そのコードをMemorySegmentのようなFFM APIコンストラクトに移行し、Arenaを使ってそのライフサイクルを管理できます。

// Using Unsafe for off-heap memory
long address = UNSAFE.allocateMemory(1024);
UNSAFE.putInt(address, 42);
int value = UNSAFE.getInt(address);
UNSAFE.freeMemory(address);

// Using MemorySegment from FFM API to achieve the same
try (Arena arena = Arena.ofShared()) {
    long byteSize = ValueLayout.JAVA_INT.byteSize();
    MemorySegment segment = arena.allocate(byteSize);
    segment.set(ValueLayout.JAVA_INT, 0, 42);
    int value = segment.get(ValueLayout.JAVA_INT, 0);
}

これらの標準APIは、ほとんどのユースケースに対して安全でパフォーマンスの高い代替手段を提供し、最新および将来のJavaバージョンとの互換性を保証します。

💡 Unsafeの依存関係を特定するのに役立つツールがあります。

  • javacが出すコンパイル時の警告を確認する。
  • コマンドラインでJDK Flight Recorder (JFR)を使用すると、プロファイル対象のJVMが最終的に非推奨となるメソッドを呼び出すたびに、jdk.DeprecatedInvocationイベントを記録する
  • JDK 23からは、新しいコマンドラインオプション --sun-misc-unsafe-memory-access={allow|warn|debug|deny} を付けてアプリケーションを実行すると、これらのメソッドの非推奨と削除が依存関係にどのような影響を与えるかを評価できる。

今後の JDK リリースでは、sun.misc.Unsafeのメモリアクセスメソッドが段階的に非推奨になります。現時点での構想は以下の通りです。

JDK 23コンパイル時および実行時に警告が表示される非推奨メソッドを導入
(デフォルト:--sun-misc-unsafe-memory-access=allow
JDK 24JEP 498により、実行時の警告がデフォルトに
(デフォルト:--sun-misc-unsafe-memory-access=warn
JDK 26サポートされていない操作はデフォルトで例外をスロー
(デフォルト:--sun-misc-unsafe-memory-access=deny
JDK 26以後このメソッドは完全に削除。
JVM は --sun-misc-unsafe-memory-access オプションを無視。

JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
https://openjdk.org/jeps/498

sun.misc.Unsafeのメモリアクセスメソッドから移行するときは、サポートされていないJDK内部メソッドに依存しないようにしてください。これは破壊的変更のリスクが増大するためです。

Prepare to Restrict the Use of JNI

JDK 1.1以降、Java Native Interface (JNI) はJavaコードとネイティブ・コード間の相互運用性を支援していました。JNIは便利ではありますが、JNIの相互作用はアプリケーションのintegrityを損なう可能性があります。

  • ネイティブ・コードを呼び出すと、JVMクラッシュを含む任意の未定義の動作につながる可能性がある。ネイティブ・コードとJavaコードは、JVMのガベージ・コレクタによって管理されないメモリ領域である、direct byte buffersを通してデータを交換することが多々ある。無効なメモリ領域によってサポートされたbyte buffersを使用すれば、確実に未定義の動作を引き起こす。
  • ネイティブ・コードはJNIを使って、JVMによるアクセス・チェックなしにフィールドにアクセスしたりメソッドを呼び出したりできる。
  • ネイティブ・コードは、(GetPrimitiveArrayCriticalのような)特定のJNI関数を誤用すると、プログラムのライフタイム中に現れる可能性のある、望ましくないガベージ・コレクターの動作を引き起こす可能性がある。

GetPrimitiveArrayCritical, ReleasePrimitiveArrayCritical
https://docs.oracle.com/en/java/javase/23/docs/specs/jni/functions.html#getprimitivearraycritical-releaseprimitivearraycritical

JNIは無効にできるJVMコンポーネントではないため、Javaコードが危険なJNI関数を使用するネイティブ・コードを呼び出さないようにする方法がありません。JNIでは、JVMはjava.lang.RuntimeクラスのloadメソッドとloadLibraryメソッドを使ってネイティブ・ライブラリをロードします。java.lang.Systemクラスの同じ名前のメソッドは、対応するRuntimeメソッドを呼び出します。ネイティブ・ライブラリのロードは、ライブラリ内で定義された初期化関数や、Javaランタイムが呼び出すJNI_OnLoad関数を使ってネイティブ・コードを実行する可能性があるため、高リスクです。このようなリスクがあるため、JDK 24はloadメソッドとloadLibraryメソッドを制限しています。

一方、FFM APIのほとんどは、設計上安全です。過去にJNIやネイティブコードを使用できた多くのシナリオは、FFM APIのメソッドを呼び出すように移行でき、Javaプラットフォームのintegrityに影響を与えません。

しかし、FFM APIを使ってネイティブ・ライブラリをロードしリンクする場合、Javaコードは、基礎となる外部関数の型と互換性のないパラメータ型を指定することで、ダウンコール・メソッド・ハンドルを要求する可能性があります。Javaでこのようなダウンコール・メソッド・ハンドルを呼び出すと、VMがクラッシュするか未定義の動作になる可能性があります。しかし、FFM APIの安全でないメソッドは、JNI関数のようなリスクをもたらしません。例えば、Javaオブジェクトのfinalフィールドの値を変更できません。デフォルトでは、FFM APIの安全でないメソッドの使用は許可されていますが、実行時に警告が表示されます。

WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by ReadFileWithFopen in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

前のスニペットでは、Javaコードが安全でない、つまり制限されたFFM APIメソッドを使用していることを警告しています。この未定義の動作とJVMクラッシュの可能性を考えると、アプリケーション開発者は、起動時に特定のJavaコードのネイティブ・アクセスを注意深く許可する必要があります。このアクションは、ネイティブ・ライブラリのロードとリンクの必要性を認め、それによって課された制限を解除します。

Restricted methods | java.lang.foreign (Java SE 23 & JDK 23)
https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/foreign/package-summary.html#restricted

💡ライブラリがJNIまたはFFM APIを利用する場合、そのドキュメントは、ネイティブ・アクセスを有効にする必要性をユーザー(アプリケーション開発者など)に知らせるべきです。あなたがそのようなライブラリを使用する開発者またはデプロイ担当者であれば、ネイティブ・アクセスを許可する責任があります。例えば、アプリケーション・コードが実行時に --enable-native-access=ALL-UNNAMED オプションを要求するライブラリを使用する場合、このオプションでクラスパス上のすべてのクラスに対し、JNI と FFM API のネイティブ・アクセス制限が解除されることに注意する必要があります。

--enable-native-access=ALL-UNNAMEDオプションは範囲が広いため、JNIまたはFFM APIを使用するJARファイルをモジュールパスに移動することで、リスクを最小限に抑え、integrityを高めることができます。この方法では、クラスパス全体ではなく、これらの JAR ファイルに対して明示的にネイティブ・アクセスを有効にできます。モジュール化されていないJARファイルをクラスパスからモジュールパスに移動すると、Javaランタイムは当該JARファイルを自動モジュールとして扱い、ファイル名に基づいて名前を付けます。

Incremental Modularization with Automatic Modules
https://dev.java/learn/modules/automatic-module/

モジュールがネイティブ・アクセスを有効にしていない場合、そのモジュール内で制限された操作を実行しようとするコードは不正とみなされます。このような操作に対するJavaランタイムの対応は、--illegal-native-accessコマンドラインオプションで指定し、以下のように動作します。

--illegal-native-access動作
allow警告や例外を出さずに操作を許可する
warn操作を許可するが、特定のモジュールで初めて不正なネイティブ・アクセスが発生したときに警告する。1モジュールにつき1回のみ警告が出る。
JDK 24ではこれが--illegal-native-accessのデフォルト・モード。
deny不正なネイティブ・アクセス操作のたびにIllegalCallerExceptionをスローする

JDK 24以前では、--enable-native-accessオプションで1つ以上のモジュールのネイティブアクセスを有効にすると、他のモジュールから制限されたFFMメソッドを呼び出そうとするとIllegalCallerExceptionが発生しました。この動作は、FFM APIをJNIに整合させるためにJDK 24で緩和されています。そのため、FFM API の不正なネイティブアクセス操作はJNIと同じ扱いを受け、例外ではなく警告が表示されます。以前の動作に戻すには、以下のオプションの組み合わせを使用します。

java --enable-native-access=Module1,... --illegal-native-access=deny ...

今後の変更に備えるため、既存のコードを--enable-native-access=denyを付けて実行し、ネイティブ・アクセスが必要なコードを特定する必要があります。

Conclusion

Java Platformのintegrityに対するコミットメントは、JEPや、リリースに統合されたすべての改善を通じて明らかです。安全でない機能を徐々に非推奨にしながら、最新の安全な代替手段を導入することで、プラットフォームはソフトウェア開発における進化するベストプラクティスに整合性を保っています。


この記事は、「JVM Programming Advent Calendar」からの転載です。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください