Will sending `kill -11` to java process raises a NullPointerException?
Asked Answered
R

1

14

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 ?

Reiff answered 15/11, 2023 at 3:20 Comment(0)
A
34

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_code will be SEGV_MAPERR;
  • for a signal sent by 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.

Aquilar answered 22/11, 2023 at 2:47 Comment(3)
Got the NullPointerException on the 3rd attempt, +1Hueyhuff
you never seize to amaze with your knowledge and perseverance. +1Tarrasa
Great answer (more accurate than mine). I have returned to you the bounty you should have had.Deluca

© 2022 - 2024 — McMap. All rights reserved.