restore_sigcontext() handed cs/ss straight to cpu_x86_load_seg(). In
protected mode that ends in helper_load_seg(), which raises #GP for an
invalid selector and calls cpu_loop_exit_restore() -- a longjmp out of
the syscall handler. Landing in cpu_exec() that way trips

    g_assert(cpu == current_cpu)

in cpu_exec_longjmp_cleanup() and aborts the whole emulator.

A guest that builds its own signal frame and executes rt_sigreturn
(SROP) leaves cs/ss zeroed, so this is easy to hit:

    $ qemu-x86_64 ./hello_srop < payload     # frame with cs=0, ss=0
    ERROR:accel/tcg/cpu-exec.c:515:cpu_exec_longjmp_cleanup:
      assertion failed: (cpu == current_cpu)
    Aborted

x86 Linux does not validate the selectors either: restore_sigcontext()
only does

    regs->cs = sc.cs | 0x03;
    regs->ss = sc.ss | 0x03;

and a bogus value matters only on the way back to user mode, where it
costs the task and not the kernel.  QEMU cannot be that casual, because
the selector goes through cpu_x86_load_seg() while the syscall handler
is still running.  So take the existing badframe path instead: accept
the selectors the CPU is already running with and return false for
anything else, which makes do_rt_sigreturn() raise SIGSEGV in the guest.

Tested with an x86_64 guest on an aarch64 host, against qemu master plus
this patch: a frame with cs=0x33, ss=0x2b restores and exits with the
requested code; a frame with cs=0, ss=0 now raises SIGSEGV in the guest
instead of aborting qemu (unpatched qemu 10.2.1 aborts with the
assertion above).

Signed-off-by: takano32 <[email protected]>
---
 linux-user/i386/signal.c | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/linux-user/i386/signal.c b/linux-user/i386/signal.c
index 9650fac940..01d54be310 100644
--- a/linux-user/i386/signal.c
+++ b/linux-user/i386/signal.c
@@ -775,8 +775,32 @@ static bool restore_sigcontext(CPUX86State *env, struct 
target_sigcontext *sc)
     env->eip = tswapl(sc->rip);
 #endif
 
-    cpu_x86_load_seg(env, R_CS, lduw_le_p(&sc->cs) | 3);
-    cpu_x86_load_seg(env, R_SS, lduw_le_p(&sc->ss) | 3);
+    /*
+     * Validate the selectors before loading them.
+     *
+     * In protected mode cpu_x86_load_seg() ends up in helper_load_seg(),
+     * which raises #GP for a bad selector and calls cpu_loop_exit_restore().
+     * That longjmps out of the syscall handler while 
cpu_exec_longjmp_cleanup()
+     * still expects the CPU it started with, so
+     *
+     *     g_assert(cpu == current_cpu)
+     *
+     * fires and the whole emulator aborts.  A guest that builds its own
+     * signal frame and executes rt_sigreturn (SROP) leaves cs/ss zeroed,
+     * so this is trivial to hit.  Linux treats such a frame as a bad frame
+     * and delivers SIGSEGV, so do the same and take the badframe path.
+     */
+    {
+        uint16_t cs = lduw_le_p(&sc->cs) | 3;
+        uint16_t ss = lduw_le_p(&sc->ss) | 3;
+
+        if (cs != (env->segs[R_CS].selector | 3) ||
+            ss != (env->segs[R_SS].selector | 3)) {
+            return false;
+        }
+        cpu_x86_load_seg(env, R_CS, cs);
+        cpu_x86_load_seg(env, R_SS, ss);
+    }
 
     tmpflags = tswapl(sc->eflags);
     env->eflags = (env->eflags & ~0x40DD5) | (tmpflags & 0x40DD5);
-- 
2.53.0


Reply via email to