Can You Even Have a Buffer Overflow in Java?
Can You Even Have a Buffer Overflow in Java?
"Buffer overflow" is one of the most famous bugs in security history. It's the classic way attackers have hijacked programs for decades - write past the end of a buffer, smash the stack, overwrite a return address, and suddenly the CPU is running code you control. So when I first wondered "how do you write a buffer overflow in Java?", the honest answer surprised me: in pure Java, you basically can't - at least not the classic, memory-corrupting kind. And understanding why taught me more about how the JVM protects you than any single feature ever did. But "can't easily" isn't "never," so this post is about both: why Java is safe by design, and the real places where buffer-overflow-style risks sneak back in.
What a classic buffer overflow actually is
In a language like C, an array is just a chunk of memory and a pointer. There's no one checking whether you stay inside the lines:
char buffer[8]; strcpy(buffer, "this string is way too long"); // writes far past 8 bytes
That strcpy happily writes past the end of buffer, trampling whatever memory sits next to it, other variables, saved registers, the function's return address. In a crafted attack, the attacker fills that adjacent memory with their own data and redirects execution. This is the bug class behind an enormous share of historical exploits. The root cause is simple: C trusts you to do your own bounds checking, and the hardware will let you write wherever you point
Why pure Java doesn't let this happen
Java was designed, from the start, to take that footgun away. Three properties do most of the work: 1. Automatic array bounds checking. Every single array access in Java is checked at runtime. If you try to read or write outside the array, the JVM doesn't corrupt adjacent memory, it throws an exception.
int[] data = new int[8]; data[20] = 42; // does NOT corrupt memory // -> throws ArrayIndexOutOfBoundsException
That exception is the whole game. Instead of silently overwriting memory, the program fails loudly and safely. There's no way to "keep writing" into whatever happens to be next door. 2. No pointer arithmetic. In C you can take a pointer and add to it to walk through memory freely. Java has no pointers you can do arithmetic on. References point at objects, and that's it, you can't nudge a reference a few bytes forward to land in someone else's data. 3. Managed memory. The JVM owns the heap. Object layout, allocation, and reclamation are all handled by the runtime and the garbage collector, not by you. You never get a raw address to misuse. Put together, these mean the classic stack-smashing buffer overflow simply isn't expressible in ordinary Java code. The trade-off is a small runtime cost for those bounds checks (which the JIT compiler often optimizes away when it can prove they're unnecessary) in exchange for eliminating an entire vulnerability class.
The exception literally named BufferOverflowException
Here's a fun twist that confuses people. Java does have a class called BufferOverflowException, but it has nothing to do with memory corruption. It lives in the java.nio package and is thrown by the NIO Buffer classes (like ByteBuffer) when you try to write past the buffer's limit:
ByteBuffer buffer = ByteBuffer.allocate(4); buffer.put((byte) 1); buffer.put((byte) 2); buffer.put((byte) 3); buffer.put((byte) 4); buffer.put((byte) 5); // -> throws java.nio.BufferOverflowException
This is the opposite of a security disaster. It's the safety mechanism working: instead of overrunning anything, the buffer refuses and throws a controlled exception. The name is a historical nod to the concept, but it describes a logical guard, not memory unsafety. Good to know, so you don't panic when you see it in a stack trace. So where do the real risks come back? Java's safety guarantees hold for pure Java. The moment you step outside the managed world, the old rules return. These are the places worth watching: 1. Native code through JNI The Java Native Interface lets Java call into C/C++ libraries. Inside that native code, you're back in unmanaged territory with full pointer freedom and zero JVM bounds checking. A buffer overflow in a JNI library is a real buffer overflow, with all the classic consequences. If your application depends on native libraries, their memory-safety bugs are your problem too. 2. sun.misc.Unsafe For years, performance-critical libraries reached for sun.misc.Unsafe, an internal API that allows direct, unchecked memory access, reading and writing at raw addresses, allocating off-heap memory, and so on. As the name warns, it bypasses the safety net. Misuse can corrupt memory or crash the JVM. It was never meant for application code, and the platform has been steadily moving people off it. 3. The Foreign Function & Memory API The modern, supported replacement for a lot of Unsafe/JNI use cases is the Foreign Function & Memory (FFM) API (Project Panama, finalized in recent Java versions). It's deliberately safer, memory segments are bounds-checked and have spatial and temporal bounds, but it still lets you reach into off-heap and native memory and call native functions. Used carefully it's much safer than Unsafe; used carelessly, especially when calling into native code, you can still trigger the same native-memory problems. 4. The JVM and native libraries themselves The JVM is itself written in C++, and many standard libraries (image decoding, font rendering, compression) have native components. Over the years there have been real CVEs that were buffer overflows in that native layer. Your Java code is safe, but the runtime executing it is C++, and C++ can have C++ bugs. This is why keeping your JDK patched and up to date is a genuine security control, not just hygiene.
The C# / .NET angle
As soon as we run the program, it loads all the Runtime classes into
C# is also a managed, memory-safe language by default: arrays are bounds-checked, there's no free pointer arithmetic in safe code, and the CLR owns memory just like the JVM does. Writing past an array throws IndexOutOfRangeException rather than corrupting memory, the direct analog of Java's ArrayIndexOutOfBoundsException.
The difference is that C# makes its escape hatches more first-class and visible:
The unsafe keyword plus fixed lets you use real pointers and pointer arithmetic in clearly marked regions, explicitly opting out of safety.
stackalloc allocates on the stack; misused in an unsafe context, it can overrun just like C.
Span
Takeaways
A classic, memory-corrupting buffer overflow cannot be written in ordinary Java, array bounds checking and the absence of pointer arithmetic prevent it by design. java.nio.BufferOverflowException is a controlled exception, not memory unsafety. Don't confuse the name with the C-style bug. Real memory-safety risk returns at the boundaries: JNI/native code, sun.misc.Unsafe, the FFM API, JVM-level CVEs, and integer overflow in size calculations. Keep your JDK patched, many real-world "Java" buffer overflows were actually in the native layer of the runtime. C#/.NET works the same way: safe by default, risky only in unsafe/P/Invoke code, the same principle in different syntax. The big lesson for me was that memory safety in Java isn't a single feature you can point at, it's the combination of bounds checking, no pointer arithmetic, and managed memory, all working together to make a whole bug class unwriteable. And the corollary: the moment you leave that managed world, you're responsible for the safety the JVM used to hand you for free.
That's it, stay safe!
Luka Vukovic