A Java OutofMemoryError
occurs when the program attempts to use more heap space than is available. Among other causes, this error may result from the following:
- A memory leak (see MSC04-J. Do not leak memory)
- An infinite loop
- Limited amounts of default heap memory available
- Incorrect implementation of common data structures (hash tables, vectors, and so on)
- Unbounded deserialization
- Writing a large number of objects to an
ObjectOutputStream
(see SER10-J. Avoid memory and resource leaks during serialization) - Creating a large number of threads.
- Uncompressing a file (see IDS04-J. Safely extract files from ZipInputStream)
Some of these causes are platform-dependent and difficult to anticipate. Others, such as reading data from a file, are fairly easy to anticipate. As a result, programs must not accept untrusted input in a manner that can cause the program to exhaust memory.
Noncompliant Code Example (readLine()
)
This noncompliant code example reads lines of text from a file and adds each one to a vector until a line with the word "quit" is encountered:
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(); } }
The code places no upper bounds on the memory space required to execute the program. Consequently, the program can easily exhaust the available heap space in two ways. First, an attacker can supply arbitrarily many lines in the file, causing the vector to grow until memory is exhausted. Second, an attacker can simply supply an arbitrarily long line, causing the readLine()
method to exhaust memory. According to the Java API documentation [API 2014], the BufferedReader.readLine()
method
Reads a line of text. A line is considered to be terminated by any one of a line feed ('
\n
'), a carriage return ('\r
'), or a carriage return followed immediately by a linefeed.
Any code that uses this method is susceptible to a resource exhaustion attack because the user can enter a string of any length.
Compliant Solution (Limited File Size)
This compliant solution imposes a limit on the size of the file being read. The limit is set with the Files.size()
method, which was introduced in Java SE 7. If the file is within the limit, we can assume the standard readLine()
method will not exhaust memory, nor will memory be exhausted by the while
loop.
class ReadNames { // ... Other methods and variables 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); } }
Compliant Solution (Limited Length Input)
This compliant solution imposes limits both on the length of each line and on the total number of items to add to the vector. (It does not depend on any Java SE 7 or later features.)
class ReadNames { // ... Other methods and variables 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 ((sb.length() > 0) ? sb.toString() : 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(); } } }
The readLimitedLine()
method takes a numeric limit, indicating the total number of characters that may exist on one line. If a line contains more characters, the line is truncated, and the characters are returned on the next invocation. This prevents an attacker from exhausting memory by supplying input with no line breaks.
Noncompliant Code Example
In a server-class machine using a parallel garbage collector, the default initial and maximum heap sizes are as follows for Java SE 6 [Sun 2006]:
- Initial heap size: larger of 1/64 of the machine's physical memory or some reasonable minimum.
- Maximum heap size: smaller of 1/4 of the physical memory or 1GB.
This noncompliant code example requires more memory on the heap than is available by default:
/* Assuming the heap size as 512 MB * (calculated as 1/4 of 2GB RAM = 512MB) * Considering long values being entered (64 bits each, * the max number of elements would be 512MB/64 bits = * 67108864) */ public class ReadNames { // Accepts unknown number of records 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 { // Adding unknown number of records to a list // The user can enter more IDs than the heap can support and, // as a result, exhaust the heap. Assume that the record ID // is a 64-bit long value 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(); } }
Compliant Solution
A simple compliant solution is to reduce the number of names to read:
// ... int count = 10000000; // ...
Compliant Solution
The OutOfMemoryError
can be avoided by ensuring the absence of infinite loops, memory leaks, and unnecessary object retention. When memory requirements are known ahead of time, the heap size can be tailored to fit the requirements using the following runtime parameters [Java 2006]:
java -Xms<initial heap size> -Xmx<maximum heap size>
For example,
java -Xms128m -Xmx512m ReadNames
Here the initial heap size is set to 128MB and the maximum heap size to 512MB.
These settings can be changed either using the Java Control Panel or from the command line. They cannot be adjusted through the application itself.
Risk Assessment
Assuming infinite heap space can result in denial of service.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
MSC05-J | Low | Probable | Medium | P4 | L3 |
Related Vulnerabilities
The Apache Geronimo bug described by GERONIMO-4224 results in an OutOfMemoryError
exception thrown by the WebAccessLogViewer
when the access log file size is too large.
Automated Detection
Tool | Version | Checker | Description |
---|---|---|---|
CodeSonar | 8.1p0 | JAVA.ALLOC.LEAK.NOTSTORED | Closeable Not Stored (Java) |
Related Guidelines
Resource Exhaustion [XZP] | |
CWE-400, Uncontrolled Resource Consumption ("Resource Exhaustion") |
Bibliography
[API 2014] | |
Java—The Java Application Launcher, Syntax for Increasing the Heap Size | |
[Oracle 2015] | Tuning the Java Runtime System |
[SDN 2008] | |
[Sun 2006] | Garbage Collection Ergonomics, Default Values for the Initial and Maximum Heap Size |
17 Comments
David Svoboda
Kalpana,
Kalpana Chatnani
David,
I have fixed the example to depend on external input and the Javadocs just mention raising the error on running out of heap memory. I checked the forums on Sun's web site, and found that they give the same solution. I wanted to confirm if I can cite these forums.
I also had a doubt: Dhruv Mohindra had entered this as a rule but it was empty and marked in red (I have updated it as a recommendation as you suggested). Would my work count towards the assignment? Please do let me know your thoughts.
Regards,
Kalpana
David Svoboda
Kalpana, this will count as one of your rules for the assignment.
I think you need 2 noncompliant code examples: One that uses a fixed amount of memory, which is more than the default maximum heap, consequently exhausting the heap. This is solved by your current compliant solution.
The other uses arbitarily large memory. Your current noncompliant solution will do, although you might consider Dhruv's comment.
As for solving this problem, you need to consider that the vector of strings can grow larger than any fixed heap, and thus you should provide a disk-based solution, such as a database. I think you can settle for discussing how to solve this problem without providing actual working code.
Kalpana Chatnani
David,
I found the Java Docs which show ways to increase the default heap size
There are two examples now:
1. fixed large amount of memory, which is fixed by increasing the default heap size
2. arbitrary sized input where the input is written to a database instead of being maintained in a data structure in the program.
I also added more comments about how I calculated the fixed size of my structure which will exhaust the heap. do let me know what you think.
Kalpana
David Svoboda
Much better, Kalpana. Only one issue remains: I suspect the remediation cost is high, at least for replacing memory-based solutions with disk-based solutions.
Dhruv Mohindra
You could add that implementing common data structures (like hashtables) incorrectly can also lead to this OutOfMemoryError. For example, someone could forget that a complete database is not a good idea to have in memory, especially when it is growing rapidly.
David Svoboda
This rule can be made normative. The normative text is something like "do not allow an attacker to provide input that requires arbitrarily large memory to process". The IDS rule about ZIP files is an instance of this rule. The 2nd NCCE/CS is nonnormative; could still keep it, but separate it from the 1st and 3rd CS.
David Svoboda
The Serialization section has this title for a nonexistant rule:
SER14-J. Do not use the writeUnshared and readUnshared methods
I've studied readUnshared() and writeUnshared(). They do serialization except that they don't recognize references to previous objects. IOW they ignore any cycles in your object hierarchy. I suppose the danger then is that readUnshared could exhaust memory, which is covered by this rule. Best suggestion is to make a NCCE/CS about readUnshared (perhaps using readObject instead in the CS). In our copious free time, of course.
Dhruv Mohindra
But I don't see the CS using the readLine() method, then why this text there?
David Svoboda
The CS uses readLine() in the read function as the NCCE, which used readLine(). I swapped the two CS's around to make this more clear.
Unni Vemanchery Mana
In the compliant solution , it is mentioned to use Java SE 7 Files.size() method. Why cannot we use length() method that is provided by File object.Is there any issue in using length() method of File object? If not, is it a compliant solution?(Just in case older versions of java are used).
David Svoboda
The
File.length()
method may be used instead ofFiles.size()
if you are using an older version of Java. If you are using Java 7,Files.size()
is preferred. TheFiles.size()
API documentation contains the following:No such text is provided for
File.length()
in Java 7 or earlier. So the value you will get fromFiles.size()
more accurately reflects the number of bytes you can read.Unni Vemanchery Mana
David,
I checked Oracle comment for length() method of File object.It is like this(This is for earlier versions of Java 7):
"Returns the length of the file denoted by this abstract pathname.
The return value is unspecified if this pathname denotes a directory.
@return The length, in bytes, of the file denoted by this abstract
pathname, or <code>0L</code> if the file does not exist. Some
operating systems may return <code>0L</code> for pathnames
denoting system-dependent entities such as devices or pipes"
In FileSystem.java, there is an abstract method called getLength(File f)(that is called from length()) whose description is as follows:
" Return the length in bytes of the file denoted by the given abstract
pathname, or zero if it does not exist, is a directory, or some other
I/O error occurs."
David Svoboda
Unni:
My Java 7 implementation (1.7.0-ea) has the same API doc text you cite. Since I haven't studied the JRE source code, it is possible that File.length() and Files.size() actually do the same thing...it's just that Files.size() promises more.
I added a check to the Java 7 Compliant Solution to throw an exception if the size is 0, because this means you are dealing with a special file or some other beast whose length cannot be determined, and thus might not be within the limit.
Masaki Kubo
In
addNames()
method of the Compliant Solution (Limited Length Input),reader
,names
andinput
seem to be unknown in the scope.David Svoboda
It is supposed to use the variables defined at the top of the class in the noncompliant code example. I tweaked the comments to make this clearer.
Masaki Kubo
Okay, thanks David!