For example, the HotSpot JVM implement null-pointer detection by catching SIGSEGV signal. So if we manually generate a SIGSEGV from external, will that also be recognized as NullPointerException in some circumstances ?
Yes, in some marginal cases an external kill command may cause a bogus NullPointerException in a Java application. This behavior is platform-dependent and difficult to reproduce, however, I managed to trigger this in practice.
HotSpot JVM employs a technique called "implicit null check", where the JVM compiles an access to an object field which offset is less than a page size (4096) to a single load/store instruction without extra overhead for checking the object reference for null. If such an instruction is executed for null reference, the OS raises SIGSEGV. The JVM's signal handler catches this signal and transfers control to the code that throws NullPointerException.
Not every SIGSEGV ends up with a NPE. HotSpot signal handler checks that
In theory, if we craft a signal that satisfies all the conditions, HotSpot will treat it as NPE.
To increase chances of a user signal hitting the right instruction, we'll write an infinite loop that repeatedly stores to an object field. To prevent hoisting of the null check, the reference itself should be loaded from a volatile field.
public class BogusNPE {
static volatile BogusNPE X = new BogusNPE();
int n;
public static void main(String[] args) {
while (true) {
BogusNPE x0 = X, x1 = X, x2 = X, x3 = X, x4 = X, x5 = X, x6 = X, x7 = X, x8 = X, x9 = X;
x0.n = x1.n = x2.n = x3.n = x4.n = x5.n = x6.n = x7.n = x8.n = x9.n = 0;
}
}
}
Here I generated 10 stores in a row, all with an implicit null check.
Use -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly to verify that the corresponding mov instructions are annotated with implicit exception:
0x00007fb4a4bd440c: mov 0x70(%r10),%edx ;*getstatic X {reexecute=0 rethrow=0 return_oop=0}
; - BogusNPE::main@32 (line 8)
0x00007fb4a4bd4410: mov 0x70(%r10),%ebp ;*getstatic X {reexecute=0 rethrow=0 return_oop=0}
; - BogusNPE::main@37 (line 8)
0x00007fb4a4bd4414: mov 0x70(%r10),%eax ;*getstatic X {reexecute=0 rethrow=0 return_oop=0}
; - BogusNPE::main@42 (line 8)
0x00007fb4a4bd4418: mov %r12d,0xc(%r12,%rax,8) ; implicit exception: dispatches to 0x00007fb4a4bd4456
;*putfield n {reexecute=0 rethrow=0 return_oop=0}
; - BogusNPE::main@66 (line 9)
0x00007fb4a4bd441d: mov %r12d,0xc(%r12,%rbp,8) ; implicit exception: dispatches to 0x00007fb4a4bd4468
;*putfield n {reexecute=0 rethrow=0 return_oop=0}
; - BogusNPE::main@70 (line 9)
0x00007fb4a4bd4422: mov %r12d,0xc(%r12,%rdx,8) ; implicit exception: dispatches to 0x00007fb4a4bd447c
;*putfield n {reexecute=0 rethrow=0 return_oop=0}
; - BogusNPE::main@74 (line 9)
Run the program and get its PID:
$ jps
256 BogusNPE
280 Jps
Here pid=256, but we should send the signal not to a process, but to the particular thread. ID of the main thread is usually pid+1, that is 257.
$ sudo kill -11 257
It may take several attempts before we finally achieve the goal:
Exception in thread "main" java.lang.NullPointerException: Cannot assign field "n" because "x5" is null
at BogusNPE.main(BogusNPE.java:9)
On x86 platform, I could trigger NPE without sudo, but on 64-bit platforms sudo is important. Also, it's substantial that PID of the shell where we run kill is less than 4096. And that is why.
HotSpot checks that the fault address siginfo->si_addr is located in zero page (otherwise load/store instruction requires an explicit null check). However, si_addr is set only when SIGSEGV is raised by kernel, we cannot control it with kill command. For user-generated signals, si_pid (sending process ID) and si_uid (user ID of sending process) are set instead.
By a lucky chance, siginfo_t structure contains a union, where si_addr overlaps with si_pid and si_uid.
63 31 0
+-----------------+
| si_addr |
+-----------------+
| si_uid | si_pid |
+-----------------+
So, to produce si_addr value between 0 and 4096, we need to make si_uid = 0 (that is, invoke kill by user 0 or root), and set si_pid < 4096. On 32-bit systems, si_addr overlaps with si_pid only.
If the signal misses mov instruction with an implicit null check, or if si_addr is larger than the page size, the JVM will crash with a fatal error instead of throwing NPE.
It is certainly possible to distinguish user-generated SIGSEGV from a signal caused by invalid memory access. The signal handler could just check si_code field of siginfo_t structure:
si_code will be SEGV_MAPERR;kill, tgkill or sigqueue, the code will be SI_USER, SI_TKILL or SI_QUEUE respectively.However, current HotSpot implementation does not do that, and therefore it is possible to fool the JVM using the above trick.
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