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 ?
Will sending `kill -11` to java process raises a NullPointerException?
385 Views Asked by choxsword AtThere are 3 best solutions below
On
Sending a kill -11 to a Java process will send a SIGSEGV (segmentation fault) signal to that process. SIGSEGV is a signal sent by the operating system to a process when it makes an invalid memory reference, or segmentation fault.
In the context of the Java HotSpot JVM, a NullPointerException is typically raised internally by the JVM when it detects an attempt to dereference a null reference. This is often implemented by catching a SIGSEGV signal that results from such an attempt. The JVM has a mechanism to differentiate between a SIGSEGV that is a legitimate NullPointerException and other segmentation faults.
When you externally send a SIGSEGV to a Java process (using kill -11), it's not equivalent to the JVM internally detecting a null reference access. Instead, it's an abrupt signal to the process that it has attempted to access memory that it shouldn't, which is typically outside the scope of normal Java exception handling.
To answer your questions:
- Will sending
kill -11to a Java process raise aNullPointerException?: No, sending akill -11(orSIGSEGV) to a Java process will not raise aNullPointerException. Instead, it will likely cause the JVM to crash or terminate unexpectedly because it's a signal that the process has attempted to access an invalid memory location. - Is the JVM capable of detecting whether a
SIGSEGVis external or due to a null access?: Yes, the JVM is generally capable of distinguishing between aSIGSEGVcaused by a genuine null pointer dereference within the Java program (which would result in aNullPointerException) and other causes ofSIGSEGV, such as an externalkillcommand or other invalid memory accesses. The JVM uses its internal mechanisms to determine the context of the SIGSEGV and whether it corresponds to a null reference access. - Can an external SIGSEGV be confused for a null access?: Under normal circumstances, an external
SIGSEGVshould not be confused with a null access within the JVM. The JVM's signal handlers are designed to interpret the context of the fault and distinguish between different causes ofSIGSEGV. However, in complex systems or in cases of JVM bugs, unexpected behavior can occur, but this would be highly unusual and not the norm.
On
Summary
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.
Background
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
- the current thread is a Java thread;
- SIGSEGV occurs in a JIT-compiled code;
- the address being accessed is within zero page (0x0 - 0xfff);
- the fault instruction is marked as "implicit exception" and there is an exception handler assigned to this instruction.
In theory, if we craft a signal that satisfies all the conditions, HotSpot will treat it as NPE.
Practice
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)
Nuances
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.
Can JVM detect the source of SIGSEGV?
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:
- for a real NullPointerException,
si_codewill beSEGV_MAPERR; - for a signal sent by
kill,tgkillorsigqueue, the code will beSI_USER,SI_TKILLorSI_QUEUErespectively.
However, current HotSpot implementation does not do that, and therefore it is possible to fool the JVM using the above trick.
It should not: a
NullPointerExceptionis a specific exception that occurs when an application tries to use an object reference that has the null value.Yet, from JavaSE 17 / Troubleshooting guide / Handle Signals and Exceptions
That approach allows the JVM to optimize performance by reducing the overhead of explicit null checks in the code, relying instead on the operating system's memory protection mechanisms to detect access to null references. When such access occurs, the operating system generates a
SIGSEGVsignal, which the JVM then interprets as an attempt to dereference a null pointer, leading to the throwing of aNullPointerException.However, it is important to note that this is an internal mechanism of the JVM and is distinct from externally generated
SIGSEGVsignals, such as those sent using thekillcommand. ExternalSIGSEGVsignals are generally used to indicate serious errors, including invalid memory access, and are more likely to result in a JVM crash or core dump rather than aNullPointerException.Again, it should not, but this is an implementation-specific aspect of JVM behavior.
That means the likelihood of such confusion happening in practice may vary depending on the JVM version, the specific code being executed, and the state of the JVM at the time of the signal.
See for instance "How does the JVM know when to throw a NullPointerException"
In that scenario, it should be easy to distinguish a trapped signal from within the code execution, from a received signal from the OS.
Also: "Can a
SIGSEGVin Java not crash the JVM?"For instance:
The hotspot had a major refactoring around signal handling in JDK-8255711, resulting in commit dd8e4ff.
The current code is
os_linux_x86.cpp#PosixSignals::pd_hotspot_signal_handlerThe JVM uses various checks to determine the context of a
SIGSEGVsignal. However, I do not see a straightforward mechanism to distinguish an externally sentSIGSEGVfrom one internally generated due to a null reference access.The signal handler examines the execution context, including the program counter and the stack, to infer the cause of the
SIGSEGV. In case of a null reference, it looks for specific patterns that suggest a null pointer exception. But if an externalSIGSEGVhappens to coincide precisely with a situation where the JVM's execution state resembles that of a null pointer access, distinguishing between the two can be challenging.However, such a scenario is relatively unlikely due to the level of precision required in timing.