target/i386: 16- and 32-bit IDIV of the minimum dividend by -1 crashes QEMU with a host SIGFPE
Host environment
- Operating system: Ubuntu, Linux 7.0, x86-64 (AMD Ryzen 9 9950X)
- QEMU: master at a1c27ca0 (11.1.50),
configured with
--target-list=x86_64-linux-user,x86_64-softmmu; also 10.2.1 (Debian 1:10.2.1+ds-1ubuntu3.2) - QEMU flags: none for linux-user (
qemu-x86_64 ./idiv 32); for system mode,-accel tcg -machine q35 -m 256M -nographic -no-reboot -kernel bzImage -initrd initramfs.cpio
Emulated CPU
Default (qemu64), TCG.
Description of problem
IDIV r/m16 with DX:AX = 0x8000:0x0000 and IDIV r/m32 with
EDX:EAX = 0x80000000:0x00000000, each dividing by -1, should raise #DE,
because the quotient (2^15 or 2^31) does not fit the destination. Linux
delivers #DE to the guest as SIGFPE with si_code == FPE_INTDIV.
Instead, QEMU itself dies of a host SIGFPE:
qemu-x86_64: QEMU internal SIGFPE {code=INTDIV, addr=0x7aab002b0f0a}
Floating point exception (core dumped)helper_idivw_AX and helper_idivl_EAX in target/i386/tcg/int_helper.c
perform the division on the host before checking whether the quotient fits:
void helper_idivw_AX(CPUX86State *env, target_ulong t0)
{
int num, den, q, r;
num = (env->regs[R_EAX] & 0xffff) | ((env->regs[R_EDX] & 0xffff) << 16);
den = (int16_t)t0;
...
q = (num / den); /* INT32_MIN / -1 */
if (q != (int16_t)q) {
raise_exception_ra(env, EXCP00_DIVZ, GETPC());
}void helper_idivl_EAX(CPUX86State *env, target_ulong t0)
{
int den, r;
int64_t num, q;
...
q = (num / den); /* INT64_MIN / -1 */
r = (num % den);
if (q != (int32_t)q) {
raise_exception_ra(env, EXCP00_DIVZ, GETPC());
}INT32_MIN / -1 and INT64_MIN / -1 are undefined behaviour in C, and on an
x86 host they compile to an idiv that traps. On hosts whose division does
not trap (AArch64's sdiv returns INT_MIN), the following check happens to
raise #DE, so the crash is presumably limited to x86 hosts.
The 8-bit form is not affected (its INT16_MIN / -1 is done in int), and the
64-bit form is not affected (idiv64 checks before it divides); the
reproducer shows both raising #DE correctly.
The same helpers serve qemu-system-x86_64 under TCG, so a user-mode
(CPL 3) guest process takes the whole VM down. Booting a minimal Linux 7.2.6
guest (-accel tcg -machine q35 -nographic) whose init forks a child for
each IDIV form: the 64-bit form raises #DE in the guest as it should; the
16-bit or 32-bit form kills qemu-system-x86_64 with SIGFPE (exit status
136), printing nothing. With -accel kvm all three forms raise #DE in the
guest.
The same class of bug was fixed for s390x (commit 0103cb1c) and is reported for m68k in #2078.
Steps to reproduce
- Save the program below as
idiv.cand build it on an x86-64 host:gcc -O2 -static -o idiv idiv.c - Run
./idiv 32natively. It printsguest SIGFPE, FPE_INTDIV: correctand exits 0. - Run
qemu-x86_64 ./idiv 32(or./idiv 16). QEMU printsQEMU internal SIGFPE {code=INTDIV, ...}and is killed by SIGFPE (exit status 136).
./idiv 8 and ./idiv 64 run correctly both natively and under QEMU.
/* IDIV of the minimum dividend by -1 must raise #DE (SIGFPE, FPE_INTDIV).
* Build: gcc -O2 -static -o idiv idiv.c
* Run: ./idiv 8 | 16 | 32 | 64, natively and under qemu-x86_64. */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void on_fpe(int sig, siginfo_t *si, void *uc)
{
(void)sig, (void)uc;
static const char ok[] = "guest SIGFPE, FPE_INTDIV: correct\n";
static const char other[] = "guest SIGFPE, unexpected si_code\n";
if (si->si_code == FPE_INTDIV)
_exit(write(1, ok, sizeof ok - 1) < 0);
_exit(2 + (write(1, other, sizeof other - 1) < 0));
}
int main(int argc, char **argv)
{
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_sigaction = on_fpe;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGFPE, &sa, 0);
int w = argc > 1 ? atoi(argv[1]) : 32;
printf("idiv%d: minimum dividend / -1\n", w);
fflush(stdout);
if (w == 8) /* AX = 0x8000 */
__asm__ volatile("mov $0x8000, %%eax; mov $-1, %%ecx; idiv %%cl"
::: "eax", "ecx", "cc");
else if (w == 16) /* DX:AX = 0x8000:0000 */
__asm__ volatile("mov $0x8000, %%edx; xor %%eax, %%eax; mov $-1, %%ecx; idiv %%cx"
::: "eax", "ecx", "edx", "cc");
else if (w == 32) /* EDX:EAX = 0x80000000:00000000 */
__asm__ volatile("mov $0x80000000, %%edx; xor %%eax, %%eax; mov $-1, %%ecx; idiv %%ecx"
::: "eax", "ecx", "edx", "cc");
else /* RDX:RAX = 0x8000000000000000:0 */
__asm__ volatile("mov $1, %%edx; shl $63, %%rdx; xor %%eax, %%eax; mov $-1, %%rcx; idiv %%rcx"
::: "rax", "rcx", "rdx", "cc");
puts("no fault: wrong");
return 1;
}Additional information
A possible fix: do the 16-bit division in int64_t (sign-extending the
32-bit dividend), where INT32_MIN / -1 is defined, and reject the one
overflowing 32-bit case before dividing:
void helper_idivw_AX(CPUX86State *env, target_ulong t0)
{
int64_t num, q;
int den, r;
num = (int32_t)((env->regs[R_EAX] & 0xffff) |
((env->regs[R_EDX] & 0xffff) << 16));
...
void helper_idivl_EAX(CPUX86State *env, target_ulong t0)
{
...
if (den == 0 || (num == INT64_MIN && den == -1)) {
raise_exception_ra(env, EXCP00_DIVZ, GETPC());
}With this change on master, the reproducer raises #DE for all three widths
in both qemu-x86_64 and qemu-system-x86_64. The two changed helpers were
also checked, as standalone C under UBSan, against a 128-bit reference on 20
million dividend/divisor pairs including the minimum dividends and -1, and a
randomized comparison of about 850 x86-64 instruction forms (2.5 million
tests, all DIV and IDIV forms among them) against a semantics model shows no
change. I can send it as a patch if that helps.
Found while validating an x86 semantics model against QEMU, real AMD hardware and Intel SDE; the hardware and SDE both raise #DE here.