Java の OutOfMemoryError は、プログラムが、使用可能なサイズを超えるヒープメモリを使おうとしたときに発生する。その原因として、たとえば以下のようなものが挙げられる。
- メモリリーク (「MSC04-J. メモリリークしない」を参照)
- 無限ループ
- ヒープメモリのデフォルトの容量が小さい
- ハッシュテーブルやベクタなど、よく使われるデータ構造の誤った実装
- 限度なしに復元を行う
- ObjectOutputStream に大量のオブジェクトを書き込む (「SER10-J. シリアライズの過程でメモリリークやリソースリークをしない」を参照)
- 大量のスレッドを作成する
- 圧縮ファイルの解凍 (「IDS04-J. ZipInputStream からファイルを安全に展開する」を参照)
これらのうちのいくつかはプラットフォーム依存であり、OutOfMemoryError の発生を予測することは難しい。一方、ファイルからのデータ読み込みなどの場合は、簡単に予測できる。プログラムでは、ヒープメモリを使い果たす危険のあるやり方で信頼できない入力を受け付けてはいけない。
違反コード (readLine())
以下の違反コード例では、ファイルからテキストの読込みを行っている。ファイルから一行読み込んではベクターに代入する操作を、"quit" からなる行を読み込むまで繰り返す。
class ReadNames { private Vector<String> names = new Vector<String>(); private final InputStreamReader input; private final BufferedReader reader; public ReadNames(String filename) throws IOException { this.input = new FileReader(filename); this.reader = new BufferedReader(input); } public void addNames() throws IOException { try { String newName; while (((newName = reader.readLine()) != null) && !(newName.equalsIgnoreCase("quit"))) { names.addElement(newName); System.out.println("adding " + newName); } } finally { input.close(); } } public static void main(String[] args) throws IOException { if (args.length != 1) { System.out.println("Arguments: [filename]"); return; } ReadNames demo = new ReadNames(args[0]); demo.addNames(); } }
このコードでは、消費するメモリ容量の上限を定めていない。そのため、ヒープメモリを浪費させる攻撃が2種類考えられる。ひとつは、大量の行数のファイルを与えることで、ベクタを増大させメモリを使い果たさせる方法である。もうひとつは、非常に長い行を与えることで、readLine() メソッドがメモリを使い果たすように仕向けるというものである。Java API ドキュメントでは、BufferedReader.readLine() メソッドは以下のように説明されている[API 2006]。
テキスト行を読み込む。1行の終端は、改行('\n')か、復帰('\r')、または復帰とそれに続く改行のいずれかで認識される。
このメソッドを使うコードはリソース枯渇攻撃を受ける危険がある。ユーザは任意の長さの文字列を入力できるからである。
適合コード (Java SE 7: ファイルサイズを制限する)
以下の適合コードでは、 Java SE 7 で新たに導入された Files.size() メソッドを使って、読み込むファイルのサイズに上限を設けている。ファイルサイズが制限内であれば、readLine() メソッドや while ループでメモリを使い果たさないことを保証できる。
// ...その他のメソッドと変数の宣言 class ReadNames { public static final int fileSizeLimit = 1000000; public ReadNames(String filename) throws IOException { long size = Files.size( Paths.get( filename)); if (size > fileSizeLimit) { throw new IOException("File too large"); } else if (size == 0L) { throw new IOException("File size cannot be determined, possibly too large"); } this.input = new FileReader(filename); this.reader = new BufferedReader(input); } }
適合コード (入力長を制限する)
以下の適合コードでは、入力する一行の文字数とベクタに追加する行数に上限を設けている。このコードでは、Java SE 7 の機能には依存していない。
class ReadNames { // ... その他のメソッドと変数の宣言 public static String readLimitedLine(Reader reader, int limit) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < limit; i++) { int c = reader.read(); if (c == -1) { return null; } if (((char) c == '\n') || ((char) c == '\r')) { break; } sb.append((char) c); } return sb.toString(); } public static final int lineLengthLimit = 1024; public static final int lineCountLimit = 1000000; public void addNames() throws IOException { try { String newName; for (int i = 0; i < lineCountLimit; i++) { newName = readLimitedLine(reader, lineLengthLimit); if (newName == null || newName.equalsIgnoreCase("quit")) { break; } names.addElement(newName); System.out.println("adding " + newName); } } finally { input.close(); } } }
readLimitedLine() メソッドは、一行の文字数の最大値を引数にとる。読み込む行がそれより多くの文字を含んでいる場合、残りの文字は次の行の読み込みに使われる。このような処理を行うことで、改行を含まない入力を与えてメモリを消費させる攻撃はできなくなる。
違反コード
並列ガベージコレクタを使うサーバクラスのマシンでは、Java SE 6 のデフォルトの初期ヒープサイズと最大ヒープサイズは以下の通りである[Sun 2006]。
- 初期ヒープサイズ: マシンの物理メモリの 1/64 か、妥当な最小サイズかの大きい方。
- 最大ヒープサイズ: 物理メモリの 1/4 か、1GB かの小さい方。
以下の違反コード例では、デフォルトの設定で使用可能な容量を超えるメモリが必要となる。
/* ヒープサイズは 512 MB とする * (2 GB RAM の 1/4th = 512 MB) * long 型の値が入力されることを考慮 (各64 ビット、 * 要素の最大数は 512 MB/64bits = 67108864) */ public class ReadNames { // 未知の数のレコードを受け付ける Vector<Long> names = new Vector<Long>(); long newID = 0L; int count = 67108865; int i = 0; InputStreamReader input = new InputStreamReader(System.in); Scanner reader = new Scanner(input); public void addNames() { try { do { // 未知の数のレコードをリストに追加する // ユーザはヒープが格納できる以上の ID を入力することができる。 // その結果、ヒープ領域が枯渇する。 Assume that the record ID // レコードID は64ビットlong型の値であると仮定する。 System.out.print("Enter recordID (To quit, enter -1): "); newID = reader.nextLong(); names.addElement(newID); i++; } while (i < count || newID != -1); } finally { input.close(); } } public static void main(String[] args) { ReadNames demo = new ReadNames(); demo.addNames(); } }
適合コード
簡単な対策は、読み込む数を減らすことである。
// ... int count = 10000000; // ...
適合コード
無限ループやメモリリークを防ぎ、不要なオブジェクトを到達可能なままにしなければ、OutOfMemoryError の発生を防ぐことができる。 必要となるメモリ量が実行前に分かっている場合、以下のように java 実行時のコマンドライン引数を指定することにより、ヒープサイズを調整することが可能である。
java -Xms<初期ヒープサイズ> -Xmx<最大ヒープサイズ>
たとえば、初期ヒープサイズを 128MB に、最大ヒープサイズを 512MB に設定するには以下のようにする。
java -Xms128m -Xmx512m ReadNames
これらの設定は Java コントロールパネルあるいはコマンドラインから変更することができる。Java アプリケーションが自分で変更することはできない。
リスク評価
ヒープメモリが無限にあると想定していると、DoS 攻撃を受ける危険がある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
MSC05-J | 低 | 中 | 中 | P4 | L3 |
関連する脆弱性
GERONIMO-4224 で記載されている Apache Geronimo のバグは、アクセスログファイルのサイズが大き過ぎる場合、WebAccessLogViewer によって OutOfMemoryError 例外がスローされるものであった。
関連ガイドライン
CERT C Secure Coding Standard | MEM11-C. Do not assume infinite heap space |
CERT C++ Secure Coding Standard | MEM12-CPP. Do not assume infinite heap space |
ISO/IEC TR 24772:2010 | Resource Exhaustion [XZP] |
MITRE CWE | CWE-400. Uncontrolled resource consumption ("resource exhaustion") |
CWE-770. Allocation of resources without limits or throttling |
参考文献
[API 2006] | Class ObjectInputStream and ObjectOutputStream |
[Java 2006] | java – The Java application launcher, Syntax for increasing the heap size |
[SDN 2008] | Serialization FAQ |
[Sun 2003] | Chapter 5, Tuning the Java Runtime System, Tuning the Java Heap |
[Sun 2006] | Garbage Collection Ergonomics, Default values for the Initial and Maximum Heap Size |
翻訳元
これは以下のページを翻訳したものです。
MSC05-J. Do not exhaust heap space (revision 109)