Going over some presentation, I've come across the following claim: When the JVM loads a class, it can analyze its content and make sure there's no overflow or underflow of the operand stack. I've found a lot of sources that make the same claim, but without specifying how it's done.
It is unclear to me how such verification can be made using static analysis. Say I have a (malicious) method that gets some value as an argument, and uses it to perform a series of pops. At load time, the number of iterations is not known, as it depends on the argument given by the method's caller. Therefore, it seems to me that only at runtime should it be possible to determined whether there will be an underflow or not. What am I missing here?
The byte code verification aims to enforce static security constraints on Java-based mobile code: the applet. Such a code can be downloaded and executed on a personal device running a browser. This popular model raises security issues concerning the access to personal information on the client side.
The bytecode verifier acts as a sort of gatekeeper: it ensures that code passed to the Java interpreter is in a fit state to be executed and can run without fear of breaking the Java interpreter. Imported code is not allowed to execute by any means until after it has passed the verifier's tests.
After the class loader in the JVM loads the byte code of . class file to the machine the Bytecode is first checked for validity by the verifier and this process is called as verification.
The bytecode verifier traverses the bytecodes, constructs the type state information, and verifies the types of the parameters to all the bytecode instructions.
You can find basic description of the Bytecode Verifier in Java Virtual Machine specification.
To put it simple, stack depth is known at every branching point, and two execution paths merging at the same merge point must also have the same stack depth. So, the verifier won't allow you to perform series of pops without corresponding puts.
The code of the method is split into execution blocks. A "block" is a sequence of instructions that can be executed without jumping out or into. The blocks build a directed graph of all possible execution paths.
A block always expects a certain stack size at its beginning and has a fixed stack size at its end (beginning + all the pushes - all the pops). The verifier checks that for all blocks 'a' that can be reached from a given block 'b', the end stack-size of b matches the beginning stack-size of a.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With