[PATCH] target/hppa: restore lazy FPU context isolation

Kirill A. Korinsky posted 1 patch 7 hours ago
hw/hppa/machine.c                     |   2 +
target/hppa/cpu.c                     |   1 +
target/hppa/cpu.h                     |   1 +
target/hppa/helper.h                  |   1 +
target/hppa/mem_helper.c              |  15 ++
target/hppa/translate.c               |  59 +++-
tests/functional/hppa/test_seabios.py |   6 +-
tests/qtest/hppa-fpu-test.c           | 371 ++++++++++++++++++++++++++
tests/qtest/meson.build               |   2 +
9 files changed, 454 insertions(+), 4 deletions(-)
create mode 100644 tests/qtest/hppa-fpu-test.c
[PATCH] target/hppa: restore lazy FPU context isolation
Posted by Kirill A. Korinsky 7 hours ago
From: "Kirill A. Korinsky" <kirill@korins.ky>

Raise assist emulation trap 22 when CCR disables FP execution;
this restores lazy FPU context isolation, stabilizes OpenBSD/hppa, and
fixes unexplained SSH "incorrect signature" failures.

I keep FP enabled on firmware entry because SeaBIOS uses FP registers
before configuring CCR.

The regression test is based on lazy.elf and eager.elf, compiled from
firmware.ld and fpu-state.s with OpenBSD/hppa 8.0's patched GCC 4.2.1
and BFD 2.17.

Closes: https://gitlab.com/qemu-project/qemu/-/work_items/4715
Signed-off-by: Kirill A. Korinsky <kirill@korins.ky>
---
 hw/hppa/machine.c                     |   2 +
 target/hppa/cpu.c                     |   1 +
 target/hppa/cpu.h                     |   1 +
 target/hppa/helper.h                  |   1 +
 target/hppa/mem_helper.c              |  15 ++
 target/hppa/translate.c               |  59 +++-
 tests/functional/hppa/test_seabios.py |   6 +-
 tests/qtest/hppa-fpu-test.c           | 371 ++++++++++++++++++++++++++
 tests/qtest/meson.build               |   2 +
 9 files changed, 454 insertions(+), 4 deletions(-)
 create mode 100644 tests/qtest/hppa-fpu-test.c

diff --git a/hw/hppa/machine.c b/hw/hppa/machine.c
index 0b0a8047ed..96d57b9b0f 100644
--- a/hw/hppa/machine.c
+++ b/hw/hppa/machine.c
@@ -842,6 +842,8 @@ static void hppa_machine_reset(MachineState *ms, ResetType type)
 
         cpu_set_pc(cs, firmware_entry);
         cpu[i]->env.psw = PSW_Q;
+        /* Firmware uses FP registers before configuring CCR. */
+        cpu[i]->env.cr[CR_SCRCCR] = 0xc0;
         cpu[i]->env.gr[5] = CPU_HPA + i * 0x1000;
     }
 
diff --git a/target/hppa/cpu.c b/target/hppa/cpu.c
index 9d64322398..83104ae0c7 100644
--- a/target/hppa/cpu.c
+++ b/target/hppa/cpu.c
@@ -91,6 +91,7 @@ static TCGTBCPUState hppa_get_tb_cpu_state(CPUState *cs)
 #ifdef CONFIG_USER_ONLY
     flags |= TB_FLAG_UNALIGN * !env_cpu(env)->prctl_unalign_sigbus;
 #else
+    flags |= TB_FLAG_FPU_DISABLED * !(env->cr[CR_SCRCCR] & 0xc0);
     if ((env->sr[4] == env->sr[5])
         & (env->sr[4] == env->sr[6])
         & (env->sr[4] == env->sr[7])) {
diff --git a/target/hppa/cpu.h b/target/hppa/cpu.h
index 8818d47dcd..c0ba787974 100644
--- a/target/hppa/cpu.h
+++ b/target/hppa/cpu.h
@@ -372,6 +372,7 @@ hwaddr hppa_abs_to_phys_pa2_w1(uint8_t phys_addr_bits, vaddr addr);
 #define TB_FLAG_PRIV_SHIFT  8
 #define TB_FLAG_UNALIGN     0x400
 #define TB_FLAG_SPHASH      0x800
+#define TB_FLAG_FPU_DISABLED 0x1000
 #define CS_BASE_DIFFPAGE    (1 << 12)
 #define CS_BASE_DIFFSPACE   (1 << 13)
 
diff --git a/target/hppa/helper.h b/target/hppa/helper.h
index 8d7fa1ae34..449818f9dd 100644
--- a/target/hppa/helper.h
+++ b/target/hppa/helper.h
@@ -82,6 +82,7 @@ DEF_HELPER_FLAGS_4(fmpynfadd_d, TCG_CALL_NO_RWG, i64, env, i64, i64, i64)
 DEF_HELPER_FLAGS_0(read_interval_timer, TCG_CALL_NO_RWG, tl)
 
 #ifndef CONFIG_USER_ONLY
+DEF_HELPER_5(fpu_mem, noreturn, env, tl, i32, i32, i32)
 DEF_HELPER_1(hppa_halt, noreturn, env)
 DEF_HELPER_1(hppa_reset, noreturn, env)
 DEF_HELPER_1(rfi, void, env)
diff --git a/target/hppa/mem_helper.c b/target/hppa/mem_helper.c
index 2393a15507..a5da0bc851 100644
--- a/target/hppa/mem_helper.c
+++ b/target/hppa/mem_helper.c
@@ -427,6 +427,21 @@ raise_exception_with_ior(CPUHPPAState *env, int excp, uintptr_t retaddr,
     cpu_loop_exit(cs);
 }
 
+G_NORETURN void HELPER(fpu_mem)(CPUHPPAState *env, target_ulong addr,
+                               uint32_t mmu_idx, uint32_t mop, uint32_t type)
+{
+    hwaddr phys;
+    int prot, excp;
+
+    /* Memory faults take priority over the assist emulation trap. */
+    excp = hppa_get_physical_address(env, addr, mmu_idx, type, mop, &phys, &prot);
+    if (excp < 0) {
+        excp = EXCP_ASSIST_EMU;
+    }
+    raise_exception_with_ior(env, excp, GETPC(), addr,
+                             MMU_IDX_MMU_DISABLED(mmu_idx));
+}
+
 void hppa_cpu_do_transaction_failed(CPUState *cs, hwaddr physaddr,
                                      vaddr addr, unsigned size,
                                      MMUAccessType access_type,
diff --git a/target/hppa/translate.c b/target/hppa/translate.c
index befdb58e9a..fcdcf5d44c 100644
--- a/target/hppa/translate.c
+++ b/target/hppa/translate.c
@@ -1711,8 +1711,30 @@ static bool do_floadw(DisasContext *ctx, unsigned rt, unsigned rb,
     return nullify_end(ctx);
 }
 
+static bool gen_fmem_excp(DisasContext *ctx, arg_ldst *a, MemOp mop, int type)
+{
+#ifndef CONFIG_USER_ONLY
+    TCGv_i64 addr, ofs;
+
+    nullify_over(ctx);
+    form_gva(ctx, &addr, &ofs, a->b, a->x,
+             a->scale ? mop & MO_SIZE : 0, a->disp, a->sp, a->m,
+             MMU_DISABLED(ctx));
+    gen_helper_fpu_mem(tcg_env, addr, tcg_constant_i32(ctx->mmu_idx),
+                       tcg_constant_i32(mop | ctx->mo_align | mo_endian(ctx)),
+                       tcg_constant_i32(type));
+    ctx->base.is_jmp = DISAS_NORETURN;
+    return nullify_end(ctx);
+#else
+    g_assert_not_reached();
+#endif
+}
+
 static bool trans_fldw(DisasContext *ctx, arg_ldst *a)
 {
+    if (ctx->tb_flags & TB_FLAG_FPU_DISABLED) {
+        return gen_fmem_excp(ctx, a, MO_UL, PAGE_READ);
+    }
     return do_floadw(ctx, a->t, a->b, a->x, a->scale ? 2 : 0,
                      a->disp, a->sp, a->m);
 }
@@ -1738,6 +1760,9 @@ static bool do_floadd(DisasContext *ctx, unsigned rt, unsigned rb,
 
 static bool trans_fldd(DisasContext *ctx, arg_ldst *a)
 {
+    if (ctx->tb_flags & TB_FLAG_FPU_DISABLED) {
+        return gen_fmem_excp(ctx, a, MO_UQ, PAGE_READ);
+    }
     return do_floadd(ctx, a->t, a->b, a->x, a->scale ? 3 : 0,
                      a->disp, a->sp, a->m);
 }
@@ -1767,6 +1792,9 @@ static bool do_fstorew(DisasContext *ctx, unsigned rt, unsigned rb,
 
 static bool trans_fstw(DisasContext *ctx, arg_ldst *a)
 {
+    if (ctx->tb_flags & TB_FLAG_FPU_DISABLED) {
+        return gen_fmem_excp(ctx, a, MO_UL, PAGE_WRITE);
+    }
     return do_fstorew(ctx, a->t, a->b, a->x, a->scale ? 2 : 0,
                       a->disp, a->sp, a->m);
 }
@@ -1787,6 +1815,9 @@ static bool do_fstored(DisasContext *ctx, unsigned rt, unsigned rb,
 
 static bool trans_fstd(DisasContext *ctx, arg_ldst *a)
 {
+    if (ctx->tb_flags & TB_FLAG_FPU_DISABLED) {
+        return gen_fmem_excp(ctx, a, MO_UQ, PAGE_WRITE);
+    }
     return do_fstored(ctx, a->t, a->b, a->x, a->scale ? 3 : 0,
                       a->disp, a->sp, a->m);
 }
@@ -2311,8 +2342,9 @@ static bool trans_mtctl(DisasContext *ctx, arg_mtctl *a)
 #endif
         break;
 
+    case CR_SCRCCR:
     case CR_EIEM:
-        /* Exit to re-evaluate interrupts in the main loop. */
+        /* Exit to re-evaluate translation flags and interrupts. */
         ctx->base.is_jmp = DISAS_IAQ_N_STALE_EXIT;
         /* FALLTHRU */
     default:
@@ -4721,6 +4753,29 @@ static void hppa_tr_insn_start(DisasContextBase *dcbase, CPUState *cs)
     ctx->insn_start_updated = false;
 }
 
+static bool decode_insn(DisasContext *ctx, uint32_t insn)
+{
+    if (ctx->tb_flags & TB_FLAG_FPU_DISABLED) {
+        switch (insn >> 26) {
+        case 0x0b:
+            /* Both floating-point uids trap when the coprocessor is disabled. */
+            insn &= ~0x40;
+            break;
+        case 0x0c:
+            if (extract32(insn, 7, 2)) {
+                break;
+            }
+            /* fall through */
+        case 0x06:
+        case 0x0e:
+        case 0x26:
+        case 0x2e:
+            return gen_excp_iir(ctx, EXCP_ASSIST_EMU);
+        }
+    }
+    return decode(ctx, insn);
+}
+
 static void hppa_tr_translate_insn(DisasContextBase *dcbase, CPUState *cs)
 {
     DisasContext *ctx = container_of(dcbase, DisasContext, base);
@@ -4754,7 +4809,7 @@ static void hppa_tr_translate_insn(DisasContextBase *dcbase, CPUState *cs)
             ret = DISAS_NEXT;
         } else {
             ctx->insn = insn;
-            if (!decode(ctx, insn)) {
+            if (!decode_insn(ctx, insn)) {
                 gen_illegal(ctx);
             }
             ret = ctx->base.is_jmp;
diff --git a/tests/functional/hppa/test_seabios.py b/tests/functional/hppa/test_seabios.py
index bdb9d534ef..9fd42d70ac 100755
--- a/tests/functional/hppa/test_seabios.py
+++ b/tests/functional/hppa/test_seabios.py
@@ -20,8 +20,10 @@ def boot_seabios(self):
         self.vm.add_args('-no-shutdown')
         self.vm.set_console()
         self.vm.launch()
-        wait_for_console_pattern(self, f'SeaBIOS PA-RISC {bits}-bit Firmware')
-        wait_for_console_pattern(self, f'Emulated machine:     HP {mach} ({bits}-bit')
+        wait_for_console_pattern(self, f'SeaBIOS PA-RISC {bits}-bit Firmware',
+                                 failure_message='SeaBIOS: Detected trap')
+        wait_for_console_pattern(self, f'Emulated machine:     HP {mach} ({bits}-bit',
+                                 failure_message='SeaBIOS: Detected trap')
 
     def test_hppa_32(self):
         self.set_machine('B160L')
diff --git a/tests/qtest/hppa-fpu-test.c b/tests/qtest/hppa-fpu-test.c
new file mode 100644
index 0000000000..af4ff2a162
--- /dev/null
+++ b/tests/qtest/hppa-fpu-test.c
@@ -0,0 +1,371 @@
+/*
+ * HPPA CCR floating-point enable and lazy context switching tests.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest.h"
+
+#define BOOT 0x1000
+#define CODE 0x1100
+#define FP_CODE 0x1300
+#define ASSIST 0x1400
+#define UNEXPECTED 0x1500
+#define HALT 0x1600
+#define DATA 0x1800
+#define RESULT 0x1900
+#define LOG 0x1a00
+
+/* Enable interruption-state collection through IPSW before entering CODE. */
+static const uint32_t bootstrap[] = {
+    0x34010010, /* ldi 8,r1 */
+    0x02c11840, /* mtctl r1,ipsw */
+    0x02201840, /* mtctl r0,iiasq */
+    0x02201840, /* mtctl r0,iiasq */
+    0x34012200, /* ldi 0x1100,r1 */
+    0x02411840, /* mtctl r1,iiaoq */
+    0x34012208, /* ldi 0x1104,r1 */
+    0x02411840, /* mtctl r1,iiaoq */
+    0x00000c00, /* rfi */
+};
+
+/*
+ * r20 counts assists. After the first call, the shared CCR writer is entered
+ * with FP enabled. Alternating its input must reselect the next translation.
+ */
+static const uint32_t fp_guest[] = {
+    0x34011000, /* ldi 0x800,r1 */
+    0x01c11840, /* mtctl r1,iva */
+    0x08000240, /* nop */
+    0x34103000, /* ldi 0x1800,r16 */
+    0x34113200, /* ldi 0x1900,r17 */
+    0x341a3400, /* ldi 0x1a00,r26 */
+    0x34140000, /* ldi 0,r20 */
+    0x34010180, /* ldi 0xc0,r1 */
+    0x01411840, /* mtctl r1,ccr */
+    0x2e00100c, /* fldd 0(r16),fr12 */
+    0x34010000, /* ldi 0,r1 */
+    0x01411840, /* mtctl r1,ccr */
+    0x08142880, /* cmpclr,= r20,r0,r0: nullify the next instruction */
+    0x08000240, /* replaced by the FP instruction under test */
+    0x6a340008, /* stw r20,4(r17) */
+    0x34010180, /* ldi 0xc0,r1 */
+    0xe8400370, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340010, /* stw r20,8(r17) */
+    0x34010000, /* ldi 0,r1 */
+    0xe8400350, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340018, /* stw r20,12(r17) */
+    0x34010180, /* ldi 0xc0,r1 */
+    0xe8400330, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340020, /* stw r20,16(r17) */
+    0x34010000, /* ldi 0,r1 */
+    0xe8400310, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340028, /* stw r20,20(r17) */
+    0x34010180, /* ldi 0xc0,r1 */
+    0xe84002f0, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340030, /* stw r20,24(r17) */
+    0x34010000, /* ldi 0,r1 */
+    0xe84002d0, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340038, /* stw r20,28(r17) */
+    0x34010180, /* ldi 0xc0,r1 */
+    0x01411840, /* mtctl r1,ccr */
+    0x2e10120c, /* fstd fr12,8(r16) */
+    0x36100010, /* ldo 8(r16),r16 */
+    0x2e10120d, /* fstd fr13,8(r16) */
+    0x34010002, /* ldi 1,r1 */
+    0x6a210000, /* stw r1,0(r17): completion */
+    0xe8000882, /* b,n 0x1600 */
+};
+
+static const uint32_t fp_site[] = {
+    0x01411840, /* mtctl r1,ccr */
+    0x08000240, /* replaced by the FP instruction under test */
+    0xe840c002, /* bv,n 0(rp) */
+};
+
+/* r20 and r26 are outside the shadow register set restored by rfir. */
+static const uint32_t assist_handler[] = {
+    0x026008a1, /* mfctl iir,r1 */
+    0x6b410000, /* stw r1,0(r26) */
+    0x024008a1, /* mfctl iiaoq,r1 */
+    0x6b410008, /* stw r1,4(r26) */
+    0x028008a1, /* mfctl isr,r1 */
+    0x6b410010, /* stw r1,8(r26) */
+    0x02a008a1, /* mfctl ior,r1 */
+    0x6b410018, /* stw r1,12(r26) */
+    0x375a0020, /* ldo 16(r26),r26 */
+    0x36940002, /* ldo 1(r20),r20 */
+    0x34010180, /* ldi 0xc0,r1 */
+    0x01411840, /* mtctl r1,ccr */
+    0x00000ca0, /* rfir: retry the faulting instruction */
+};
+
+static const uint32_t unexpected_handler[] = {
+    0x026008a1, /* mfctl iir,r1 */
+    0x6a210008, /* stw r1,4(r17) */
+    0x024008a1, /* mfctl iiaoq,r1 */
+    0x6a210010, /* stw r1,8(r17) */
+    0x028008a1, /* mfctl isr,r1 */
+    0x6a210018, /* stw r1,12(r17) */
+    0x02a008a1, /* mfctl ior,r1 */
+    0x6a210020, /* stw r1,16(r17) */
+    0x6a380000, /* stw r24,0(r17): 0x100 | exception number */
+    0xe80001aa, /* b,n 0x1600 */
+};
+
+/* r18 owns the live FP state; r19 requests the next logical context. */
+static const uint32_t lazy_guest[] = {
+    0x34011000, /* ldi 0x800,r1 */
+    0x01c11840, /* mtctl r1,iva */
+    0x08000240, /* nop */
+    0x34123000, /* ldi 0x1800,r18: owner A */
+    0x34113200, /* ldi 0x1900,r17 */
+    0x34140000, /* ldi 0,r20 */
+    0x34010180, /* ldi 0xc0,r1 */
+    0x01411840, /* mtctl r1,ccr */
+    0x2e40100c, /* fldd 0(r18),fr12 */
+    0x318c0e0c, /* fadd,d fr12,fr12,fr12: A = 2 */
+    0x34173240, /* ldi 0x1920,r23 */
+    0x34133020, /* ldi 0x1810,r19: request B */
+    0xe8400390, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x318c0e0c, /* fadd,d fr12,fr12,fr12: B = 32 */
+    0x36f70010, /* ldo 8(r23),r23 */
+    0x34133000, /* ldi 0x1800,r19: request A */
+    0xe8400368, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x318c0e0c, /* fadd,d fr12,fr12,fr12: A = 4 */
+    0x36f70010, /* ldo 8(r23),r23 */
+    0x34133020, /* ldi 0x1810,r19: request B */
+    0xe8400340, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x318c0e0c, /* fadd,d fr12,fr12,fr12: B = 64 */
+    0x36f70010, /* ldo 8(r23),r23 */
+    0x34133000, /* ldi 0x1800,r19: request A */
+    0xe8400318, /* bl 0x1300,rp */
+    0x08000240, /* nop */
+    0x6a340008, /* stw r20,4(r17) */
+    0x34010002, /* ldi 1,r1 */
+    0x6a210000, /* stw r1,0(r17) */
+    0xe80008f2, /* b,n 0x1600 */
+};
+
+static const uint32_t lazy_site[] = {
+    0x01401840, /* mtctl r0,ccr */
+    0x2ee0120c, /* fstd fr12,0(r23) */
+    0xe840c002, /* bv,n 0(rp) */
+};
+
+static const uint32_t lazy_handler[] = {
+    0x34010180, /* ldi 0xc0,r1 */
+    0x01411840, /* mtctl r1,ccr */
+    0x2e40120c, /* fstd fr12,0(r18): save owner */
+    0x2e60100c, /* fldd 0(r19),fr12: restore requested context */
+    0x08130252, /* copy r19,r18 */
+    0x36940002, /* ldo 1(r20),r20 */
+    0x00000ca0, /* rfir */
+};
+
+static void write_words(QTestState *qts, uint32_t addr,
+                        const uint32_t *words, size_t count)
+{
+    for (size_t i = 0; i < count; i++) {
+        qtest_writel(qts, addr + 4 * i, words[i]);
+    }
+}
+
+static uint32_t branch(uint32_t pc, uint32_t target)
+{
+    uint32_t d = (target - pc - 8) / 4;
+
+    return 0xe8000002 | ((d & 0x10000) >> 16) | ((d & 0xf800) << 5) |
+           ((d & 0x400) >> 8) | ((d & 0x3ff) << 3);
+}
+
+static QTestState *start_guest(bool retry_assist)
+{
+    QTestState *qts = qtest_init("-machine none -cpu pa-7300lc -m 1M "
+                                "-S -accel tcg -no-shutdown "
+                                "-device loader,addr=0x1000,cpu-num=0");
+
+    write_words(qts, BOOT, bootstrap, G_N_ELEMENTS(bootstrap));
+    /* Shutdown may re-enter this TB before the main loop stops the CPU. */
+    qtest_writel(qts, HALT, 0xfffdead0);
+    write_words(qts, UNEXPECTED, unexpected_handler,
+                G_N_ELEMENTS(unexpected_handler));
+    for (unsigned int i = 0; i < 32; i++) {
+        uint32_t pc = 0x800 + i * 32;
+        uint32_t target = retry_assist && i == 22 ? ASSIST : UNEXPECTED;
+
+        qtest_writel(qts, pc, 0x34180000 | ((0x100 | i) << 1));
+        qtest_writel(qts, pc + 4, branch(pc + 4, target));
+    }
+    return qts;
+}
+
+static void run_guest(QTestState *qts, uint32_t expected)
+{
+    uint32_t result;
+
+    qtest_qmp_assert_success(qts, "{'execute':'cont'}");
+    qtest_qmp_eventwait(qts, "SHUTDOWN");
+    result = qtest_readl(qts, RESULT);
+    g_test_message("completion=0x%x, result[1]=0x%x, result[2]=0x%x",
+                   result, qtest_readl(qts, RESULT + 4),
+                   qtest_readl(qts, RESULT + 8));
+    g_assert_cmphex(result, ==, expected);
+}
+
+typedef struct FPTest {
+    const char *name;
+    uint32_t instruction;
+    bool memory;
+    uint64_t fr13;
+} FPTest;
+
+static const FPTest fp_tests[] = {
+    { "fldw", 0x2600100c, true },
+    { "fstw", 0x2600120c, true },
+    { "fldd", 0x2e00100c, true },
+    { "fstd", 0x2e00120c, true },
+    { "fid", 0x30000000 },
+    { "fadd-double", 0x318c0e0d, false, 0x4010000000000000ULL },
+    { "fadd-double-extended", 0x398c0e0d, false, 0x4010000000000000ULL },
+    { "fmpyadd-single", 0x188541e6 }, /* fr20L,fr21L,fr22L,fr23L,fr24L */
+    { "fmpysub-double", 0x988541c6 }, /* fr4,fr5,fr6,fr7,fr8 */
+};
+
+static void test_fp(const void *opaque)
+{
+    const FPTest *test = opaque;
+    static const uint32_t counts[] = { 0, 0, 1, 1, 2, 2, 3 };
+    QTestState *qts = start_guest(true);
+
+    write_words(qts, CODE, fp_guest, G_N_ELEMENTS(fp_guest));
+    write_words(qts, FP_CODE, fp_site, G_N_ELEMENTS(fp_site));
+    write_words(qts, ASSIST, assist_handler, G_N_ELEMENTS(assist_handler));
+    qtest_writel(qts, CODE + 13 * 4, test->instruction);
+    qtest_writel(qts, FP_CODE + 4, test->instruction);
+    qtest_writeq(qts, DATA, 0x4000000000000000ULL); /* double 2 */
+    run_guest(qts, 1);
+    for (size_t i = 0; i < G_N_ELEMENTS(counts); i++) {
+        g_assert_cmpuint(qtest_readl(qts, RESULT + 4 + 4 * i), ==, counts[i]);
+    }
+    for (unsigned int i = 0; i < 3; i++) {
+        g_assert_cmphex(qtest_readl(qts, LOG + 16 * i), ==, test->instruction);
+        g_assert_cmphex(qtest_readl(qts, LOG + 16 * i + 4), ==, FP_CODE + 4);
+        if (test->memory) {
+            g_assert_cmphex(qtest_readl(qts, LOG + 16 * i + 8), ==, 0);
+            g_assert_cmphex(qtest_readl(qts, LOG + 16 * i + 12), ==, DATA);
+        }
+    }
+    g_assert_cmphex(qtest_readq(qts, DATA + 8), ==, 0x4000000000000000ULL);
+    g_assert_cmphex(qtest_readq(qts, DATA + 16), ==, test->fr13);
+    qtest_quit(qts);
+}
+
+static void test_lazy(void)
+{
+    static const uint64_t markers[] = {
+        0x4030000000000000ULL, /* B = 16 */
+        0x4000000000000000ULL, /* A = 2 */
+        0x4040000000000000ULL, /* B = 32 */
+        0x4010000000000000ULL, /* A = 4 */
+    };
+    QTestState *qts = start_guest(true);
+
+    write_words(qts, CODE, lazy_guest, G_N_ELEMENTS(lazy_guest));
+    write_words(qts, FP_CODE, lazy_site, G_N_ELEMENTS(lazy_site));
+    write_words(qts, ASSIST, lazy_handler, G_N_ELEMENTS(lazy_handler));
+    qtest_writeq(qts, DATA, 0x3ff0000000000000ULL); /* A = 1 */
+    qtest_writeq(qts, DATA + 16, markers[0]);
+    run_guest(qts, 1);
+    g_assert_cmpuint(qtest_readl(qts, RESULT + 4), ==, 4);
+    for (size_t i = 0; i < G_N_ELEMENTS(markers); i++) {
+        g_assert_cmphex(qtest_readq(qts, RESULT + 32 + 8 * i), ==, markers[i]);
+    }
+    g_assert_cmphex(qtest_readq(qts, DATA), ==, 0x4010000000000000ULL);
+    g_assert_cmphex(qtest_readq(qts, DATA + 16), ==, 0x4050000000000000ULL);
+    qtest_quit(qts);
+}
+
+static const uint32_t fault_guest[] = {
+    0x34011000, /* ldi 0x800,r1 */
+    0x01c11840, /* mtctl r1,iva */
+    0x34103000, /* ldi 0x1800,r16: replaced for unaligned cases */
+    0x34113200, /* ldi 0x1900,r17 */
+    0x01401840, /* mtctl r0,ccr */
+    0x08000240, /* replaced by ssm 2,r0 for data translation */
+    0x08000240, /* replaced by the FP instruction under test */
+    0x34010002, /* ldi 1,r1 */
+    0x6a210000, /* stw r1,0(r17): unexpectedly completed */
+    0xe80009aa, /* b,n 0x1600 */
+};
+
+typedef struct FaultTest {
+    const char *name;
+    uint32_t instruction;
+    unsigned int exception;
+    uint32_t address;
+    bool dtlb;
+    bool memory;
+} FaultTest;
+
+static const FaultTest fault_tests[] = {
+    { "unaligned-load", 0x2e00100c, 28, DATA + 2, false, true },
+    { "unaligned-store", 0x2e00120c, 28, DATA + 2, false, true },
+    { "dtlb-load", 0x2e00100c, 15, DATA, true, true },
+    { "dtlb-store", 0x2e00120c, 15, DATA, true, true },
+    { "disabled-uid1-arithmetic", 0x30000040, 22, DATA, false, false },
+    { "disabled-uid1-memory", 0x2e00104c, 22, DATA, false, true },
+};
+
+static void test_fault(const void *opaque)
+{
+    const FaultTest *test = opaque;
+    uint32_t addr = test->address;
+    QTestState *qts = start_guest(false);
+
+    write_words(qts, CODE, fault_guest, G_N_ELEMENTS(fault_guest));
+    qtest_writel(qts, CODE + 8, 0x34100000 | (addr << 1));
+    if (test->dtlb) {
+        qtest_writel(qts, CODE + 20, 0x00020d60); /* ssm 2,r0 */
+    }
+    qtest_writel(qts, CODE + 24, test->instruction);
+    run_guest(qts, 0x100 | test->exception);
+    g_assert_cmphex(qtest_readl(qts, RESULT + 4), ==, test->instruction);
+    g_assert_cmphex(qtest_readl(qts, RESULT + 8), ==, CODE + 24);
+    if (test->memory) {
+        g_assert_cmphex(qtest_readl(qts, RESULT + 12), ==, 0);
+        g_assert_cmphex(qtest_readl(qts, RESULT + 16), ==, addr);
+    }
+    qtest_quit(qts);
+}
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+    if (!qtest_has_accel("tcg")) {
+        g_test_skip("TCG accelerator required");
+        return 0;
+    }
+    for (size_t i = 0; i < G_N_ELEMENTS(fp_tests); i++) {
+        g_autofree char *name = g_strdup_printf("fpu/%s", fp_tests[i].name);
+
+        qtest_add_data_func(name, &fp_tests[i], test_fp);
+    }
+    qtest_add_func("fpu/lazy-contexts", test_lazy);
+    for (size_t i = 0; i < G_N_ELEMENTS(fault_tests); i++) {
+        g_autofree char *name = g_strdup_printf("fpu/%s", fault_tests[i].name);
+
+        qtest_add_data_func(name, &fault_tests[i], test_fault);
+    }
+    return g_test_run();
+}
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 46685d6d46..9d422886d8 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -158,6 +158,8 @@ qtests_alpha = ['boot-serial-test'] + \
 qtests_avr = [ 'boot-serial-test' ]
 
 qtests_hppa = \
+  (config_all_accel.has_key('CONFIG_TCG') and \
+   config_all_devices.has_key('CONFIG_GENERIC_LOADER') ? ['hppa-fpu-test'] : []) + \
   qtests_filter + \
   (config_all_devices.has_key('CONFIG_VGA') ? ['display-vga-test'] : [])
 
-- 
2.55.0