From 872b168fe344914550c29b4f1b0cac9f2e70e7fc Mon Sep 17 00:00:00 2001 From: Ian Lance Taylor Date: Tue, 21 Jul 2015 22:34:48 -0700 Subject: [PATCH] runtime: if we don't handle a signal on a non-Go thread, raise it In the past badsignal would crash the program. In https://golang.org/cl/10757044 badsignal was changed to call sigsend, to fix issue #3250. The effect of this was that when a non-Go thread received a signal, and os/signal.Notify was not being used to check for occurrences of the signal, the signal was ignored. This changes the code so that if os/signal.Notify is not being used, then the signal handler is reset to what it was, and the signal is raised again. This lets non-Go threads handle the signal as they wish. In particular, it means that a segmentation violation in a non-Go thread will ordinarily crash the process, as it should. Fixes #10139. Update #11794. Change-Id: I2109444aaada9d963ad03b1d071ec667760515e5 Reviewed-on: https://go-review.googlesource.com/12503 Reviewed-by: Russ Cox Run-TryBot: Ian Lance Taylor --- src/runtime/crash_cgo_test.go | 67 +++++++++++++++++++++++++++++++ src/runtime/os1_darwin.go | 5 +++ src/runtime/os1_dragonfly.go | 18 ++++++--- src/runtime/os1_freebsd.go | 18 ++++++--- src/runtime/os1_linux.go | 6 +++ src/runtime/os1_nacl.go | 4 ++ src/runtime/os1_netbsd.go | 6 +++ src/runtime/os1_openbsd.go | 5 +++ src/runtime/os1_plan9.go | 4 ++ src/runtime/os2_dragonfly.go | 11 +++-- src/runtime/os2_freebsd.go | 11 +++-- src/runtime/os2_linux.go | 2 + src/runtime/os2_solaris.go | 1 + src/runtime/os3_solaris.go | 6 +++ src/runtime/os_darwin.go | 2 +- src/runtime/os_dragonfly.go | 2 +- src/runtime/os_freebsd.go | 2 +- src/runtime/os_linux.go | 4 +- src/runtime/signal1_unix.go | 37 +++++++++++++++++ src/runtime/signal_windows.go | 6 +++ src/runtime/sigqueue.go | 10 ++++- src/runtime/sys_dragonfly_amd64.s | 6 +-- src/runtime/sys_freebsd_386.s | 7 ++-- src/runtime/sys_freebsd_amd64.s | 6 +-- src/runtime/sys_freebsd_arm.s | 6 +-- src/runtime/sys_linux_amd64.s | 2 +- 26 files changed, 215 insertions(+), 39 deletions(-) diff --git a/src/runtime/crash_cgo_test.go b/src/runtime/crash_cgo_test.go index 9ff95e6f44..d2847b0d45 100644 --- a/src/runtime/crash_cgo_test.go +++ b/src/runtime/crash_cgo_test.go @@ -82,6 +82,19 @@ func TestCgoExternalThreadSIGPROF(t *testing.T) { } } +func TestCgoExternalThreadSignal(t *testing.T) { + // issue 10139 + switch runtime.GOOS { + case "plan9", "windows": + t.Skipf("no pthreads on %s", runtime.GOOS) + } + got := executeTest(t, cgoExternalThreadSignalSource, nil) + want := "OK\n" + if got != want { + t.Fatalf("expected %q, but got %q", want, got) + } +} + func TestCgoDLLImports(t *testing.T) { // test issue 9356 if runtime.GOOS != "windows" { @@ -282,6 +295,60 @@ func main() { } ` +const cgoExternalThreadSignalSource = ` +package main + +/* +#include + +void **nullptr; + +void *crash(void *p) { + *nullptr = p; + return 0; +} + +int start_crashing_thread(void) { + pthread_t tid; + return pthread_create(&tid, 0, crash, 0); +} +*/ +import "C" + +import ( + "fmt" + "os" + "os/exec" + "time" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "crash" { + i := C.start_crashing_thread() + if i != 0 { + fmt.Println("pthread_create failed:", i) + // Exit with 0 because parent expects us to crash. + return + } + + // We should crash immediately, but give it plenty of + // time before failing (by exiting 0) in case we are + // running on a slow system. + time.Sleep(5 * time.Second) + return + } + + out, err := exec.Command(os.Args[0], "crash").CombinedOutput() + if err == nil { + fmt.Println("C signal did not crash as expected\n") + fmt.Printf("%s\n", out) + os.Exit(1) + } + + fmt.Println("OK") +} +` + const cgoDLLImportsMainSource = ` package main diff --git a/src/runtime/os1_darwin.go b/src/runtime/os1_darwin.go index 06bc2c79a5..08ec611d43 100644 --- a/src/runtime/os1_darwin.go +++ b/src/runtime/os1_darwin.go @@ -469,3 +469,8 @@ func signalstack(s *stack) { func updatesigmask(m sigmask) { sigprocmask(_SIG_SETMASK, &m[0], nil) } + +func unblocksig(sig int32) { + mask := uint32(1) << (uint32(sig) - 1) + sigprocmask(_SIG_UNBLOCK, &mask, nil) +} diff --git a/src/runtime/os1_dragonfly.go b/src/runtime/os1_dragonfly.go index a4c11d4fbf..f96c78ca80 100644 --- a/src/runtime/os1_dragonfly.go +++ b/src/runtime/os1_dragonfly.go @@ -78,7 +78,7 @@ func newosproc(mp *m, stk unsafe.Pointer) { } var oset sigset - sigprocmask(&sigset_all, &oset) + sigprocmask(_SIG_SETMASK, &sigset_all, &oset) params := lwpparams{ start_func: funcPC(lwp_start), @@ -91,7 +91,7 @@ func newosproc(mp *m, stk unsafe.Pointer) { mp.tls[0] = uintptr(mp.id) // XXX so 386 asm can find it lwp_create(¶ms) - sigprocmask(&oset, nil) + sigprocmask(_SIG_SETMASK, &oset, nil) } func osinit() { @@ -124,7 +124,7 @@ func msigsave(mp *m) { if unsafe.Sizeof(*smask) > unsafe.Sizeof(mp.sigmask) { throw("insufficient storage for signal mask") } - sigprocmask(nil, smask) + sigprocmask(_SIG_SETMASK, nil, smask) } // Called to initialize a new m (including the bootstrap m). @@ -145,14 +145,14 @@ func minit() { nmask.__bits[(i-1)/32] &^= 1 << ((uint32(i) - 1) & 31) } } - sigprocmask(&nmask, nil) + sigprocmask(_SIG_SETMASK, &nmask, nil) } // Called from dropm to undo the effect of an minit. func unminit() { _g_ := getg() smask := (*sigset)(unsafe.Pointer(&_g_.m.sigmask)) - sigprocmask(smask, nil) + sigprocmask(_SIG_SETMASK, smask, nil) signalstack(nil) } @@ -237,5 +237,11 @@ func signalstack(s *stack) { func updatesigmask(m sigmask) { var mask sigset copy(mask.__bits[:], m[:]) - sigprocmask(&mask, nil) + sigprocmask(_SIG_SETMASK, &mask, nil) +} + +func unblocksig(sig int32) { + var mask sigset + mask.__bits[(sig-1)/32] |= 1 << ((uint32(sig) - 1) & 31) + sigprocmask(_SIG_UNBLOCK, &mask, nil) } diff --git a/src/runtime/os1_freebsd.go b/src/runtime/os1_freebsd.go index 6dbf8299b4..f3519f3490 100644 --- a/src/runtime/os1_freebsd.go +++ b/src/runtime/os1_freebsd.go @@ -88,9 +88,9 @@ func newosproc(mp *m, stk unsafe.Pointer) { mp.tls[0] = uintptr(mp.id) // so 386 asm can find it var oset sigset - sigprocmask(&sigset_all, &oset) + sigprocmask(_SIG_SETMASK, &sigset_all, &oset) thr_new(¶m, int32(unsafe.Sizeof(param))) - sigprocmask(&oset, nil) + sigprocmask(_SIG_SETMASK, &oset, nil) } func osinit() { @@ -123,7 +123,7 @@ func msigsave(mp *m) { if unsafe.Sizeof(*smask) > unsafe.Sizeof(mp.sigmask) { throw("insufficient storage for signal mask") } - sigprocmask(nil, smask) + sigprocmask(_SIG_SETMASK, nil, smask) } // Called to initialize a new m (including the bootstrap m). @@ -147,14 +147,14 @@ func minit() { nmask.__bits[(i-1)/32] &^= 1 << ((uint32(i) - 1) & 31) } } - sigprocmask(&nmask, nil) + sigprocmask(_SIG_SETMASK, &nmask, nil) } // Called from dropm to undo the effect of an minit. func unminit() { _g_ := getg() smask := (*sigset)(unsafe.Pointer(&_g_.m.sigmask)) - sigprocmask(smask, nil) + sigprocmask(_SIG_SETMASK, smask, nil) signalstack(nil) } @@ -239,5 +239,11 @@ func signalstack(s *stack) { func updatesigmask(m [(_NSIG + 31) / 32]uint32) { var mask sigset copy(mask.__bits[:], m[:]) - sigprocmask(&mask, nil) + sigprocmask(_SIG_SETMASK, &mask, nil) +} + +func unblocksig(sig int32) { + var mask sigset + mask.__bits[(sig-1)/32] |= 1 << ((uint32(sig) - 1) & 31) + sigprocmask(_SIG_UNBLOCK, &mask, nil) } diff --git a/src/runtime/os1_linux.go b/src/runtime/os1_linux.go index e6942a9f79..dd64afca2c 100644 --- a/src/runtime/os1_linux.go +++ b/src/runtime/os1_linux.go @@ -333,3 +333,9 @@ func updatesigmask(m sigmask) { copy(mask[:], m[:]) rtsigprocmask(_SIG_SETMASK, &mask, nil, int32(unsafe.Sizeof(mask))) } + +func unblocksig(sig int32) { + var mask sigset + mask[(sig-1)/32] |= 1 << ((uint32(sig) - 1) & 31) + rtsigprocmask(_SIG_UNBLOCK, &mask, nil, int32(unsafe.Sizeof(mask))) +} diff --git a/src/runtime/os1_nacl.go b/src/runtime/os1_nacl.go index 66e60f8b12..143752ada8 100644 --- a/src/runtime/os1_nacl.go +++ b/src/runtime/os1_nacl.go @@ -170,6 +170,10 @@ func badsignal2() { var badsignal1 = []byte("runtime: signal received on thread not created by Go.\n") +func raisebadsignal(sig int32) { + badsignal2() +} + func madvise(addr unsafe.Pointer, n uintptr, flags int32) {} func munmap(addr unsafe.Pointer, n uintptr) {} func resetcpuprofiler(hz int32) {} diff --git a/src/runtime/os1_netbsd.go b/src/runtime/os1_netbsd.go index 2a579b8694..cacd60620b 100644 --- a/src/runtime/os1_netbsd.go +++ b/src/runtime/os1_netbsd.go @@ -230,3 +230,9 @@ func updatesigmask(m sigmask) { copy(mask.__bits[:], m[:]) sigprocmask(_SIG_SETMASK, &mask, nil) } + +func unblocksig(sig int32) { + var mask sigset + mask.__bits[(sig-1)/32] |= 1 << ((uint32(sig) - 1) & 31) + sigprocmask(_SIG_UNBLOCK, &mask, nil) +} diff --git a/src/runtime/os1_openbsd.go b/src/runtime/os1_openbsd.go index c07cd243be..24a095b9d6 100644 --- a/src/runtime/os1_openbsd.go +++ b/src/runtime/os1_openbsd.go @@ -239,3 +239,8 @@ func signalstack(s *stack) { func updatesigmask(m sigmask) { sigprocmask(_SIG_SETMASK, m[0]) } + +func unblocksig(sig int32) { + mask := uint32(1) << (uint32(sig) - 1) + sigprocmask(_SIG_UNBLOCK, mask) +} diff --git a/src/runtime/os1_plan9.go b/src/runtime/os1_plan9.go index bda7057f44..9615b6d1a4 100644 --- a/src/runtime/os1_plan9.go +++ b/src/runtime/os1_plan9.go @@ -254,6 +254,10 @@ func badsignal2() { exits(&_badsignal[0]) } +func raisebadsignal(sig int32) { + badsignal2() +} + func _atoi(b []byte) int { n := 0 for len(b) > 0 && '0' <= b[0] && b[0] <= '9' { diff --git a/src/runtime/os2_dragonfly.go b/src/runtime/os2_dragonfly.go index 0a20ed43fa..ccad82f013 100644 --- a/src/runtime/os2_dragonfly.go +++ b/src/runtime/os2_dragonfly.go @@ -5,8 +5,11 @@ package runtime const ( - _NSIG = 33 - _SI_USER = 0x10001 - _SS_DISABLE = 4 - _RLIMIT_AS = 10 + _NSIG = 33 + _SI_USER = 0x10001 + _SS_DISABLE = 4 + _RLIMIT_AS = 10 + _SIG_BLOCK = 1 + _SIG_UNBLOCK = 2 + _SIG_SETMASK = 3 ) diff --git a/src/runtime/os2_freebsd.go b/src/runtime/os2_freebsd.go index f67211fdf2..84ab715237 100644 --- a/src/runtime/os2_freebsd.go +++ b/src/runtime/os2_freebsd.go @@ -5,8 +5,11 @@ package runtime const ( - _SS_DISABLE = 4 - _NSIG = 33 - _SI_USER = 0x10001 - _RLIMIT_AS = 10 + _SS_DISABLE = 4 + _NSIG = 33 + _SI_USER = 0x10001 + _RLIMIT_AS = 10 + _SIG_BLOCK = 1 + _SIG_UNBLOCK = 2 + _SIG_SETMASK = 3 ) diff --git a/src/runtime/os2_linux.go b/src/runtime/os2_linux.go index eaa9f0e833..71f36ebeff 100644 --- a/src/runtime/os2_linux.go +++ b/src/runtime/os2_linux.go @@ -8,6 +8,8 @@ const ( _SS_DISABLE = 2 _NSIG = 65 _SI_USER = 0 + _SIG_BLOCK = 0 + _SIG_UNBLOCK = 1 _SIG_SETMASK = 2 _RLIMIT_AS = 9 ) diff --git a/src/runtime/os2_solaris.go b/src/runtime/os2_solaris.go index 26ca15f628..f5c0c83316 100644 --- a/src/runtime/os2_solaris.go +++ b/src/runtime/os2_solaris.go @@ -6,6 +6,7 @@ package runtime const ( _SS_DISABLE = 2 + _SIG_UNBLOCK = 2 _SIG_SETMASK = 3 _NSIG = 73 /* number of signals in sigtable array */ _SI_USER = 0 diff --git a/src/runtime/os3_solaris.go b/src/runtime/os3_solaris.go index 53d7b96b0f..7caa72e3be 100644 --- a/src/runtime/os3_solaris.go +++ b/src/runtime/os3_solaris.go @@ -304,6 +304,12 @@ func updatesigmask(m sigmask) { sigprocmask(_SIG_SETMASK, &mask, nil) } +func unblocksig(sig int32) { + var mask sigset + mask.__sigbits[(sig-1)/32] |= 1 << ((uint32(sig) - 1) & 31) + sigprocmask(_SIG_UNBLOCK, &mask, nil) +} + //go:nosplit func semacreate() uintptr { var sem *semt diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index c432c99208..3deafd5227 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -20,7 +20,7 @@ func mach_thread_self() uint32 func sysctl(mib *uint32, miblen uint32, out *byte, size *uintptr, dst *byte, ndst uintptr) int32 //go:noescape -func sigprocmask(sig uint32, new, old *uint32) +func sigprocmask(how uint32, new, old *uint32) //go:noescape func sigaction(mode uint32, new, old *sigactiont) diff --git a/src/runtime/os_dragonfly.go b/src/runtime/os_dragonfly.go index 60234bbdea..b19270a18d 100644 --- a/src/runtime/os_dragonfly.go +++ b/src/runtime/os_dragonfly.go @@ -19,7 +19,7 @@ func sigfwd(fn uintptr, sig uint32, info *siginfo, ctx unsafe.Pointer) func sigaction(sig int32, new, old *sigactiont) //go:noescape -func sigprocmask(new, old *sigset) +func sigprocmask(how int32, new, old *sigset) //go:noescape func setitimer(mode int32, new, old *itimerval) diff --git a/src/runtime/os_freebsd.go b/src/runtime/os_freebsd.go index b2b5cd1f3f..8c8a10661d 100644 --- a/src/runtime/os_freebsd.go +++ b/src/runtime/os_freebsd.go @@ -19,7 +19,7 @@ func sigfwd(fn uintptr, sig uint32, info *siginfo, ctx unsafe.Pointer) func sigaction(sig int32, new, old *sigactiont) //go:noescape -func sigprocmask(new, old *sigset) +func sigprocmask(how int32, new, old *sigset) //go:noescape func setitimer(mode int32, new, old *itimerval) diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index 523d28b210..bd492f5e3b 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -29,8 +29,8 @@ func rtsigprocmask(sig uint32, new, old *sigset, size int32) //go:noescape func getrlimit(kind int32, limit unsafe.Pointer) int32 -func raise(sig uint32) -func raiseproc(sig uint32) +func raise(sig int32) +func raiseproc(sig int32) //go:noescape func sched_getaffinity(pid, len uintptr, buf *uintptr) int32 diff --git a/src/runtime/signal1_unix.go b/src/runtime/signal1_unix.go index d3e9dac097..56d9755190 100644 --- a/src/runtime/signal1_unix.go +++ b/src/runtime/signal1_unix.go @@ -140,6 +140,43 @@ func sigpipe() { raise(_SIGPIPE) } +// raisebadsignal is called when a signal is received on a non-Go +// thread, and the Go program does not want to handle it (that is, the +// program has not called os/signal.Notify for the signal). +func raisebadsignal(sig int32) { + if sig == _SIGPROF { + // Ignore profiling signals that arrive on non-Go threads. + return + } + + var handler uintptr + if sig >= _NSIG { + handler = _SIG_DFL + } else { + handler = fwdSig[sig] + } + + // Reset the signal handler and raise the signal. + // We are currently running inside a signal handler, so the + // signal is blocked. We need to unblock it before raising the + // signal, or the signal we raise will be ignored until we return + // from the signal handler. We know that the signal was unblocked + // before entering the handler, or else we would not have received + // it. That means that we don't have to worry about blocking it + // again. + unblocksig(sig) + setsig(sig, handler, false) + raise(sig) + + // If the signal didn't cause the program to exit, restore the + // Go signal handler and carry on. + // + // We may receive another instance of the signal before we + // restore the Go handler, but that is not so bad: we know + // that the Go program has been ignoring the signal. + setsig(sig, funcPC(sighandler), true) +} + func crash() { if GOOS == "darwin" { // OS X core dumps are linear dumps of the mapped memory, diff --git a/src/runtime/signal_windows.go b/src/runtime/signal_windows.go index 5e17f747bd..d80cc97755 100644 --- a/src/runtime/signal_windows.go +++ b/src/runtime/signal_windows.go @@ -203,6 +203,12 @@ func sigdisable(sig uint32) { func sigignore(sig uint32) { } +func badsignal2() + +func raisebadsignal(sig int32) { + badsignal2() +} + func crash() { // TODO: This routine should do whatever is needed // to make the Windows program abort/crash as it diff --git a/src/runtime/sigqueue.go b/src/runtime/sigqueue.go index 9cfe2592db..e078bfaf0e 100644 --- a/src/runtime/sigqueue.go +++ b/src/runtime/sigqueue.go @@ -165,5 +165,13 @@ func signal_ignore(s uint32) { // This runs on a foreign stack, without an m or a g. No stack split. //go:nosplit func badsignal(sig uintptr) { - cgocallback(unsafe.Pointer(funcPC(sigsend)), noescape(unsafe.Pointer(&sig)), unsafe.Sizeof(sig)) + cgocallback(unsafe.Pointer(funcPC(badsignalgo)), noescape(unsafe.Pointer(&sig)), unsafe.Sizeof(sig)) +} + +func badsignalgo(sig uintptr) { + if !sigsend(uint32(sig)) { + // A foreign thread received the signal sig, and the + // Go code does not want to handle it. + raisebadsignal(int32(sig)) + } } diff --git a/src/runtime/sys_dragonfly_amd64.s b/src/runtime/sys_dragonfly_amd64.s index efda4326a4..3dae2a79df 100644 --- a/src/runtime/sys_dragonfly_amd64.s +++ b/src/runtime/sys_dragonfly_amd64.s @@ -307,9 +307,9 @@ TEXT runtime·osyield(SB),NOSPLIT,$-4 RET TEXT runtime·sigprocmask(SB),NOSPLIT,$0 - MOVL $3, DI // arg 1 - how (SIG_SETMASK) - MOVQ new+0(FP), SI // arg 2 - set - MOVQ old+8(FP), DX // arg 3 - oset + MOVL how+0(FP), DI // arg 1 - how + MOVQ new+8(FP), SI // arg 2 - set + MOVQ old+16(FP), DX // arg 3 - oset MOVL $340, AX // sys_sigprocmask SYSCALL JAE 2(PC) diff --git a/src/runtime/sys_freebsd_386.s b/src/runtime/sys_freebsd_386.s index 94b8d95044..be20808a0e 100644 --- a/src/runtime/sys_freebsd_386.s +++ b/src/runtime/sys_freebsd_386.s @@ -355,10 +355,11 @@ TEXT runtime·osyield(SB),NOSPLIT,$-4 TEXT runtime·sigprocmask(SB),NOSPLIT,$16 MOVL $0, 0(SP) // syscall gap - MOVL $3, 4(SP) // arg 1 - how (SIG_SETMASK) - MOVL new+0(FP), AX + MOVL how+0(FP), AX // arg 1 - how + MOVL AX, 4(SP) + MOVL new+4(FP), AX MOVL AX, 8(SP) // arg 2 - set - MOVL old+4(FP), AX + MOVL old+8(FP), AX MOVL AX, 12(SP) // arg 3 - oset MOVL $340, AX // sys_sigprocmask INT $0x80 diff --git a/src/runtime/sys_freebsd_amd64.s b/src/runtime/sys_freebsd_amd64.s index a9a621b095..8ef04588c3 100644 --- a/src/runtime/sys_freebsd_amd64.s +++ b/src/runtime/sys_freebsd_amd64.s @@ -295,9 +295,9 @@ TEXT runtime·osyield(SB),NOSPLIT,$-4 RET TEXT runtime·sigprocmask(SB),NOSPLIT,$0 - MOVL $3, DI // arg 1 - how (SIG_SETMASK) - MOVQ new+0(FP), SI // arg 2 - set - MOVQ old+8(FP), DX // arg 3 - oset + MOVL how+0(FP), DI // arg 1 - how + MOVQ new+8(FP), SI // arg 2 - set + MOVQ old+16(FP), DX // arg 3 - oset MOVL $340, AX // sys_sigprocmask SYSCALL JAE 2(PC) diff --git a/src/runtime/sys_freebsd_arm.s b/src/runtime/sys_freebsd_arm.s index 3dd04cf973..298900c9a2 100644 --- a/src/runtime/sys_freebsd_arm.s +++ b/src/runtime/sys_freebsd_arm.s @@ -327,9 +327,9 @@ TEXT runtime·osyield(SB),NOSPLIT,$-4 RET TEXT runtime·sigprocmask(SB),NOSPLIT,$0 - MOVW $3, R0 // arg 1 - how (SIG_SETMASK) - MOVW new+0(FP), R1 // arg 2 - set - MOVW old+4(FP), R2 // arg 3 - oset + MOVW how+0(FP), R0 // arg 1 - how + MOVW new+4(FP), R1 // arg 2 - set + MOVW old+8(FP), R2 // arg 3 - oset MOVW $SYS_sigprocmask, R7 SWI $0 MOVW.CS $0, R8 // crash on syscall failure diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s index 8644a0b5fd..59c21c5b42 100644 --- a/src/runtime/sys_linux_amd64.s +++ b/src/runtime/sys_linux_amd64.s @@ -219,7 +219,7 @@ TEXT runtime·rt_sigaction(SB),NOSPLIT,$0-36 RET TEXT runtime·sigfwd(SB),NOSPLIT,$0-32 - MOVQ sig+8(FP), DI + MOVL sig+8(FP), DI MOVQ info+16(FP), SI MOVQ ctx+24(FP), DX MOVQ fn+0(FP), AX -- 2.50.0