向 java 进程发送“kill -11”会引发 NullPointerException 吗?

will sending `kill -11` to java process raises a NullPointerException?

提问人:choxsword 提问时间:11/15/2023 更新时间:11/19/2023 访问量:124

问:

赏金将在 4 天后到期。这个问题的答案有资格获得 +100 声望赏金。Choxsword 希望引起人们对这个问题的更多关注
任何JVM专家请帮助我解决这个问题。

例如,HotSpot JVM 通过捕获信号来实现 NullPointer 检测。那么,如果我们从外部手动生成一个 SIGSEGV,在某些情况下也会被识别为 NullPointerException 吗?SIGSEGV

Java Linux JVM 热点

评论


答:

3赞 VonC 11/17/2023 #1

发送到 java 进程会引发 NullPointerException 吗?kill -11

它不应该:a 是应用程序尝试使用具有 null 值的对象引用时发生的特定异常。NullPointerException

然而,来自 JavaSE 17 / 故障排除指南 / 处理信号和异常

Java HotSpot VM 安装信号处理程序以实现各种功能并处理致命错误情况。

例如,在优化中,为了避免在很少抛出的情况下进行显式空检查,捕获并处理信号,然后抛出信号。java.lang.NullPointerExceptionSIGSEGVNullPointerException

一般来说,信号/陷阱发生的情况分为两类:

  • 当信号被预期和处理时,例如隐式 null 处理。另一个示例是安全点轮询机制,该机制在需要安全点时保护内存中的页面。访问该页面的任何线程都会导致 ,这会导致执行将线程带到安全点的存根。SIGSEGV

  • 意想不到的信号。这包括在 VM 代码、Java 本机接口 (JNI) 代码或本机代码中执行时。在这些情况下,信号是意外的,因此调用致命错误处理来创建错误日志并终止进程。SIGSEGV

这种方法允许 JVM 通过减少代码中显式 null 检查的开销来优化性能,而是依靠操作系统的内存保护机制来检测对 null 引用的访问。当发生此类访问时,操作系统会生成一个信号,然后 JVM 会将其解释为尝试取消引用空指针,从而导致抛出 .SIGSEGVNullPointerException

但是,需要注意的是,这是 JVM 的内部机制,与外部生成的信号(例如使用命令发送的信号)不同。外部信号通常用于指示严重错误,包括无效的内存访问,并且更可能导致 JVM 崩溃或核心转储,而不是 .SIGSEGVkillSIGSEGVNullPointerException

+---------------------+         +-----------------------------------+
| External Process    |         | Java Process running on HotSpot   |
| sending SIGSEGV     | ------> | JVM                               |
| (kill -11)          |         | Likely JVM Crash or Core Dump     |
+---------------------+         +-----------------------------------+

JVM 是否总是能够检测外部访问是否为外部访问,或者当外部访问发生在特定时间时,即当预期潜在的空访问时,是否有可能将外部混淆为空访问?SIGSEGVSIGSEGVSIGSEGV

同样,它不应该,但这是 JVM 行为的一个特定于实现的方面。
这意味着在实践中发生这种混淆的可能性可能会有所不同,具体取决于 JVM 版本、正在执行的特定代码以及发出信号时 JVM 的状态。

例如,请参阅“JVM 如何知道何时抛出 NullPointerException"

JVM 可以使用虚拟内存硬件实现空检查。JVM 安排其虚拟地址空间中的第 0 页映射到不可读 + 不可写的页面。

由于 null 表示为零,因此当 Java 代码尝试取消引用 null 时,这将尝试访问不可寻址的页面,并导致操作系统向 JVM 发送“段错误”信号。

JVM 的段错误信号处理程序可以捕获它,找出代码的执行位置,并在相应线程的堆栈上创建并抛出 NPE。

在这种情况下,应该很容易区分代码执行中的捕获信号和来自操作系统的接收信号。

另外:“Java 中的 SIGSEGV 不会使 JVM 崩溃吗?"

在某些情况下,JVM 的信号处理程序可能会将事件转换为 Java 异常。
如果 JVM 硬崩溃无法发生,您只会得到 JVM 硬崩溃;例如,如果触发的线程在事件发生时正在执行本机库中的代码。
SIGSEGVSIGSEGVSIGSEGV

例如

HotSpot JVM 在启动时特意生成 SIGSEGV 以检查某些 CPU 功能。没有开关可以将其关闭。我建议完全跳过,因为 JVM 在许多情况下将它用于自己的目的。SIGSEGVgdb


如果堆栈恰好位于外部触发时访问地址怎么办?SIGSEGV

热点在 JDK-8255711 中对信号处理进行了重大重构,导致提交 dd8e4ff

当前代码为 os_linux_x86.cpp#PosixSignals::p d_hotspot_signal_handler

  // decide if this trap can be handled by a stub
  address stub = nullptr;

  address pc          = nullptr;

  //%note os_trap_1
  if (info != nullptr && uc != nullptr && thread != nullptr) {
    pc = (address) os::Posix::ucontext_get_pc(uc);

    if (sig == SIGSEGV && info->si_addr == 0 && info->si_code == SI_KERNEL) {
      // An irrecoverable SI_KERNEL SIGSEGV has occurred.
      // It's likely caused by dereferencing an address larger than TASK_SIZE.
      return false;
    }

    // Handle ALL stack overflow variations here
    if (sig == SIGSEGV) {
      address addr = (address) info->si_addr;

      // check if fault address is within thread stack
      if (thread->is_in_full_stack(addr)) {
        // stack overflow
        if (os::Posix::handle_stack_overflow(thread, addr, pc, uc, &stub)) {
          return true; // continue
        }
      }
    }

    if ((sig == SIGSEGV) && VM_Version::is_cpuinfo_segv_addr(pc)) {
      // Verify that OS save/restore AVX registers.
      stub = VM_Version::cpuinfo_cont_addr();
    }

    if (thread->thread_state() == _thread_in_Java) {
      // Java thread running in Java code => find exception handler if any
      // a fault inside compiled code, the interpreter, or a stub

      if (sig == SIGSEGV && SafepointMechanism::is_poll_address((address)info->si_addr)) {
        stub = SharedRuntime::get_poll_stub(pc);
      } else if (sig == SIGBUS /* && info->si_code == BUS_OBJERR */) {
        // BugId 4454115: A read from a MappedByteBuffer can fault
        // here if the underlying file has been truncated.
        // Do not crash the VM in such a case.
        CodeBlob* cb = CodeCache::find_blob(pc);
        CompiledMethod* nm = (cb != nullptr) ? cb->as_compiled_method_or_null() : nullptr;
        bool is_unsafe_arraycopy = thread->doing_unsafe_access() && UnsafeCopyMemory::contains_pc(pc);
        if ((nm != nullptr && nm->has_unsafe_access()) || is_unsafe_arraycopy) {
          address next_pc = Assembler::locate_next_instruction(pc);
          if (is_unsafe_arraycopy) {
            next_pc = UnsafeCopyMemory::page_error_continue_pc(pc);
          }
          stub = SharedRuntime::handle_unsafe_access(thread, next_pc);
        }
      }
      else

#ifdef AMD64
      if (sig == SIGFPE  &&
          (info->si_code == FPE_INTDIV || info->si_code == FPE_FLTDIV)) {
        stub =
          SharedRuntime::
          continuation_for_implicit_exception(thread,
                                              pc,
                                              SharedRuntime::
                                              IMPLICIT_DIVIDE_BY_ZERO);
#else
      if (sig == SIGFPE /* && info->si_code == FPE_INTDIV */) {
        // HACK: si_code does not work on linux 2.2.12-20!!!
        int op = pc[0];
        if (op == 0xDB) {
          // FIST
          // TODO: The encoding of D2I in x86_32.ad can cause an exception
          // prior to the fist instruction if there was an invalid operation
          // pending. We want to dismiss that exception. From the win_32
          // side it also seems that if it really was the fist causing
          // the exception that we do the d2i by hand with different
          // rounding. Seems kind of weird.
          // NOTE: that we take the exception at the NEXT floating point instruction.
          assert(pc[0] == 0xDB, "not a FIST opcode");
          assert(pc[1] == 0x14, "not a FIST opcode");
          assert(pc[2] == 0x24, "not a FIST opcode");
          return true;
        } else if (op == 0xF7) {
          // IDIV
          stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_DIVIDE_BY_ZERO);
        } else {
          // TODO: handle more cases if we are using other x86 instructions
          //   that can generate SIGFPE signal on linux.
          tty->print_cr("unknown opcode 0x%X with SIGFPE.", op);
          fatal("please update this code.");
        }
#endif // AMD64
      } else if (sig == SIGSEGV &&
                 MacroAssembler::uses_implicit_null_check(info->si_addr)) {
          // Determination of interpreter/vtable stub/compiled code null exception
          stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL);
      }
    } else if ((thread->thread_state() == _thread_in_vm ||
                thread->thread_state() == _thread_in_native) &&
               (sig == SIGBUS && /* info->si_code == BUS_OBJERR && */
               thread->doing_unsafe_access())) {
        address next_pc = Assembler::locate_next_instruction(pc);
        if (UnsafeCopyMemory::contains_pc(pc)) {
          next_pc = UnsafeCopyMemory::page_error_continue_pc(pc);
        }
        stub = SharedRuntime::handle_unsafe_access(thread, next_pc);
    }

    // jni_fast_Get<Primitive>Field can trap at certain pc's if a GC kicks in
    // and the heap gets shrunk before the field access.
    if ((sig == SIGSEGV) || (sig == SIGBUS)) {
      address addr = JNI_FastGetField::find_slowcase_pc(pc);
      if (addr != (address)-1) {
        stub = addr;
      }
    }
  }

JVM 使用各种检查来确定信号的上下文。但是,我没有看到一种简单的机制来区分外部发送的和内部生成的,因为引用访问为空。SIGSEGVSIGSEGV

信号处理程序检查执行上下文(包括程序计数器和堆栈),以推断 .如果引用为 null,它会查找建议 null 指针异常的特定模式。但是,如果外部恰好与 JVM 的执行状态类似于空指针访问的情况重合,则区分两者可能具有挑战性。SIGSEGVSIGSEGV

但是,由于计时所需的精确度,这种情况相对不太可能发生。

评论

3赞 Holger 11/17/2023
这并不能回答一个问题,即JVM是否总是能够检测外部访问是否是外部访问,或者当访问发生在特定时间时,即当预期有潜在访问时,是否有可能将外部与访问混淆。SIGSEGVSIGSEGVSIGSEGVnullnull
0赞 VonC 11/17/2023
@Holger我已经编辑了答案以解决您的评论。
0赞 choxsword 11/17/2023
@Holger JVM 总是能正确地检测到这一点?因为外部信号可能随时发生,包括 JVM 认为自己处于 Nullpointer 状态的那一刻
0赞 VonC 11/17/2023
@choxsword 是的,但它通过不同的执行路径(每个路径都有自己的执行堆栈)发生。JVM 应该能够区分 SIGSEV 的来源。
1赞 Holger 11/17/2023
@choxsword我知道,这就是问题所在,如果我确定知道答案,我会发布一个答案。我想,操作系统必须能够告诉JVM哪个线程触发了信号,并且由于外部信号缺少关联的线程,因此它们必须是可区分的。
-1赞 Moziii 11/19/2023 #2

向 Java 进程发送 将向该进程发送(分段错误)信号。 是操作系统在进程发出无效的内存引用或分段错误时发送到进程的信号。kill -11SIGSEGVSIGSEGV

在 Java HotSpot JVM 的上下文中,当 JVM 检测到取消引用空引用的尝试时,通常会在内部引发 a。这通常是通过捕获此类尝试产生的信号来实现的。JVM 有一种机制来区分合法错误和其他分段错误。NullPointerExceptionSIGSEGVSIGSEGVNullPointerException

当您从外部向 Java 进程发送 (using ) 时,这并不等同于 JVM 在内部检测空引用访问。相反,它向进程发出一个突然的信号,表明它试图访问它不应该访问的内存,这通常超出了正常 Java 异常处理的范围。SIGSEGVkill -11

要回答您的问题,请执行以下操作:

  1. 向 Java 进程发送 kill -11 是否会引发 NullPointerException:否,向 Java 进程发送 (或 ) 不会引发 .相反,它可能会导致 JVM 崩溃或意外终止,因为它是进程尝试访问无效内存位置的信号。kill -11SIGSEGVNullPointerException
  2. JVM 是否能够检测 SIGSEGV 是外部的还是由于空访问?:是的,JVM 通常能够区分 Java 程序中真正的空指针取消引用(这将导致 )和其他原因,例如外部命令或其他无效的内存访问。JVM 使用其内部机制来确定 SIGSEGV 的上下文以及它是否对应于空引用访问。SIGSEGVNullPointerExceptionSIGSEGVkill
  3. 外部 SIGSEGV 可以与空访问混淆吗?:在正常情况下,外部 SIGSEGV 不应与 JVM 中的空访问混淆。JVM 的信号处理程序旨在解释故障的上下文并区分 的不同原因。但是,在复杂的系统或 JVM 错误的情况下,可能会发生意外行为,但这是非常不寻常的,而不是常态。SIGSEGVSIGSEGV
12赞 apangin 11/22/2023 #3

总结

是的,在某些边缘情况下,外部命令可能会导致 Java 应用程序中出现虚假命令。这种行为依赖于平台并且难以重现,但是,我设法在实践中触发了它。killNullPointerException

背景

HotSpot JVM 采用一种称为“隐式空检查”的技术,其中 JVM 将对偏移量小于页面大小 (4096) 的对象字段的访问编译为单个加载/存储指令,而不会产生额外的开销来检查对象引用。如果执行此类指令以供参考,则操作系统会引发 .JVM 的信号处理程序捕获此信号,并将控制权传递给抛出 .nullnullSIGSEGVNullPointerException

并非每个人都以 NPE 告终。HotSpot 信号处理程序会检查SIGSEGV

  • 当前线程是 Java 线程;
  • SIGSEGV 出现在 JIT 编译的代码中;
  • 被访问的地址在零页(0x0 - 0xfff)以内;
  • 错误指令被标记为“隐式异常”,并且为此指令分配了一个异常处理程序。

从理论上讲,如果我们制作一个满足所有条件的信号,HotSpot 会将其视为 NPE。

实践

为了增加用户信号命中正确指令的机会,我们将编写一个无限循环,重复存储到对象字段。为了防止取消空检查,应从易失性字段加载引用本身。

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;
        }
    }
}

在这里,我连续生成了 10 个存储,所有存储都带有隐式 null 检查。

用于验证相应的指令是否标注了:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblymovimplicit 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)

运行程序并获取其 PID:

$ jps
256 BogusNPE
280 Jps

这里 pid=256,但我们不应该将信号发送到进程,而是发送到特定的线程。主线程的 ID 通常为 pid+1,即 257。

$ sudo kill -11 257

在我们最终实现目标之前,可能需要多次尝试:

Exception in thread "main" java.lang.NullPointerException: Cannot assign field "n" because "x5" is null
        at BogusNPE.main(BogusNPE.java:9)

细微 差别

在 x86 平台上,我可以在没有 的情况下触发 NPE,但在 64 位平台上很重要。此外,我们运行的 shell 的 PID 小于 4096,这一点很重要。这就是原因。sudosudokill

HotSpot 检查错误地址是否位于零页中(否则加载/存储指令需要显式 null 检查)。但是,只有当内核引发 SIGSEGV 时才设置,我们无法用命令控制它。对于用户生成的信号,设置(发送进程 ID)和(发送进程的用户 ID)。siginfo->si_addrsi_addrkillsi_pidsi_uid

幸运的是,结构包含一个并集,其中与 和 重叠。siginfo_tsi_addrsi_pidsi_uid

63       31       0
+-----------------+
|     si_addr     |
+-----------------+
| si_uid | si_pid |
+-----------------+

因此,要产生介于 0 和 4096 之间的值,我们需要 make (即由用户 0 调用 kill 或 ),并将 .在 32 位系统上,仅与 重叠。si_addrsi_uid = 0rootsi_pid < 4096si_addrsi_pid

如果信号错过了带有隐式 null 检查的指令,或者如果大于页面大小,则 JVM 将崩溃并出现致命错误,而不是抛出 NPE。movsi_addr

JVM可以检测SIGSEGV的来源吗?

当然,可以将用户生成的 SIGSEGV 与无效内存访问引起的信号区分开来。信号处理程序可以只检查siginfo_t结构的字段:si_code

  • 对于真正的 NullPointerException,将是si_codeSEGV_MAPERR;
  • 对于由 或 发送的信号,代码将分别为 、 或。killtgkillsigqueueSI_USERSI_TKILLSI_QUEUE

但是,当前的 HotSpot 实现无法做到这一点,因此可以使用上述技巧来欺骗 JVM。

评论

3赞 Holger 11/23/2023
在第 3 次尝试时得到,+1NullPointerException
1赞 Eugene 11/23/2023
你永远不会用你的知识和毅力来惊叹。+1
1赞 VonC 11/27/2023
很棒的答案(比我的更准确)。我已经把你本该得到的赏金还给了你。