QEMU v10 适配 AFL:架构变更与 MIPS 延迟槽 Bug 分析

本文将探讨将 qemuafl 的补丁应用到 QEMU v10.x 版本时可能遇到的困难及解决方案。

前文分析了 AFL++ 对 QEMU 5.X 版本的补丁内容,以及在 QEMU 5.X 版本中通过 ioctl 操作 nvram 时可能出现的未知 Bug。鉴于此,计划将 qemuafl 的补丁迁移至 QEMU 的最新版本中。

QEMU 当前最新版本为:v10.1.3。

在尝试将 qemuafl 的补丁迁移至该版本时,遇到了因 QEMU 架构变更导致的严重问题。

在新版本的 QEMU 中,为了加速文件的编译速度,对代码结构进行了区分。例如,一部分代码的编译只依赖主机架构,而另一部分代码的编译则依赖目标架构。

accel/tcg 目录下的代码为例,在最新版的 QEMU 中,该部分被认为仅需考虑主机架构,因此只需编译一次。例如在首次编译 mips 架构的 QEMU 后,再次编译 arm 架构时,这部分代码无需重新编译,仅需编译与架构相关的代码。

然而,qemuafl 的补丁在仅需考虑主机架构的目录代码中引入了对目标架构参数的依赖,从而导致编译失败。

研究发现,自 v10.1 版本起,QEMU 已完全变更为新架构。若在此版本中添加 AFL 相关补丁,需要进行大量修改。进一步分析发现,v10.0.6 版本的 QEMU 架构尚未完全变更,若在该架构中进行相关补丁适配,可节省大量时间。

以下是需要修改的代码部分说明。

首先是 accel/tcg 目录下的代码,accel/tcg/meson.build 文件的部分代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
common_ss.add(when: 'CONFIG_TCG', if_true: files(
'cpu-exec-common.c',
'tcg-runtime.c',
'tcg-runtime-gvec.c',
))
tcg_specific_ss = ss.source_set()
tcg_specific_ss.add(files(
'tcg-all.c',
'cpu-exec.c',
'tb-maint.c',
'translate-all.c',
'translator.c',
))

common_ss 集合中的文件表明该部分代码是通用代码,无需考虑目标架构。tcg_specific_ss 集合中的代码为需要考虑目标架构的 tcg 代码。

tcg-runtime.h 的代码中,补丁内容大量使用了 tl 类型,如下所示:

1
DEF_HELPER_FLAGS_2(qasan_load1, TCG_CALL_NO_RWG, void, env, tl)

将宏展开后可以发现,tl 类型的定义位于 include/exec/helper-head.h.inc 文件中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define dh_alias(t) glue(dh_alias_, t)

#ifdef COMPILING_PER_TARGET
# ifdef TARGET_LONG_BITS
# if TARGET_LONG_BITS == 32
# define dh_alias_tl i32
# define dh_typecode_tl dh_typecode_i32
# else
# define dh_alias_tl i64
# define dh_typecode_tl dh_typecode_i64
# endif
# endif
# define dh_ctype_tl target_ulong
#endif /* COMPILING_PER_TARGET */

dh_alias(t) => dh_alias_tl,该类型只有当存在 COMPILING_PER_TARGET 定义时,才会被声明。

通过最外层的 meson.build 代码可以看到,只有在编译需要考虑目标架构的代码时,才会设置该值,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
...
foreach target : target_dirs
config_target = config_target_mak[target]
target_name = config_target['TARGET_NAME']
target_base_arch = config_target['TARGET_BASE_ARCH']
arch_srcs = [config_target_h[target]]
arch_deps = []
c_args = ['-DCOMPILING_PER_TARGET',
'-DCONFIG_TARGET="@0@-config-target.h"'.format(target),
]
...

因此,需要将 tcg-runtime.ctcg-runtime.h 文件中 AFL 补丁的代码提取出来,放入 tcg-runtime_afl.ctcg-runtime_afl.h 文件中。

在这部分的补丁代码中,受影响最大的是 TCP Helper 函数,该部分代码的实现逻辑在前文中已说明,此处不再赘述。

tcg-runtime_afl.c 文件中,代码主要是 Helper 函数的实现,为了防止 missing-prototypes 错误,需要添加以下头文件:

1
2
3
#define HELPER_H  "accel/tcg/tcg-runtime_afl.h"
#include "exec/helper-proto.h.inc"
#undef HELPER_H

经测试,需包含的最简头文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "qemu/osdep.h"
#include "qemu/host-utils.h"
#include "exec/cpu-common.h"
#include "user/page-protection.h"
#include "accel/tcg/getpc.h"
#include "user/abitypes.h"
#include "exec/cpu-all.h"

#include "qemuafl/common.h"
#include "tcg/tcg-op.h"
#include "qemuafl/qemu-ijon-support.h"

#define HELPER_H "accel/tcg/tcg-runtime_afl.h"
#include "exec/helper-proto.h.inc"
#include "exec/helper-info.c.inc"
#undef HELPER_H

最后修改 accel/tcg/meson.build,如下所示:

1
2
3
4
5
6
7
8
tcg_specific_ss.add(files(
'tcg-all.c',
'cpu-exec.c',
'tb-maint.c',
'translate-all.c',
'translator.c',
'tcg-runtime_afl.c'
))

QASAN核心代码位于tcg目录下,该部分代码也发生了变化,代码从tcg/tcg-op.c文件迁移到了tcg/tcg-op-ldst.c文件中。并且tcg目录下的代码都为通用代码,不考虑目标架构,但是AFL patch的头文件需要考虑目标架构,因此还需要对头文件进行修改。

build_qemu_support.sh 兼容性修改

QEMU v10.0.6 不存在以下配置,需从 build_qemu_support.sh 文件中删除:

1
2
3
4
5
6
7
--disable-blobs
--disable-live-block-migration
--disable-sheepdog
--disable-vhost-scsi
--disable-vhost-vsock
--disable-vnc-png
--disable-xfsctl

qemuafl/asan-giovese-inl.h

qemuafl/asan-giovese-inl.h 文件中需要添加一个声明,如下所示:

1
2
void queue_signal(CPUArchState *env, int sig, int si_type,
target_siginfo_t *info);

函数变化

  1. tcg_const_tl -> tcg_constant_tl
  2. tcg_const_ptr->tcg_constant_ptr
  3. tcg_constant_tl 得到的值不再需要使用 tcg_temp_free 释放,该 free 函数也已不再存在。因此需要搜索所有 tcg_temp_free 调用并将其注释。考虑到 AFL 补丁代码中该部分可能仍需释放,因此进行了以下修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// include/tcg/tcg-op.h
#include "tcg/tcg-temp-internal.h"
#if TARGET_LONG_BITS == 32
typedef TCGv_i32 TCGv;
#define tcg_temp_new() tcg_temp_new_i32()
#define tcg_temp_free(v) tcg_temp_free_i32(v) // 添加32位的free
#define tcg_global_mem_new tcg_global_mem_new_i32
#define tcgv_tl_temp tcgv_i32_temp
#define tcg_gen_qemu_ld_tl tcg_gen_qemu_ld_i32
#define tcg_gen_qemu_st_tl tcg_gen_qemu_st_i32
#elif TARGET_LONG_BITS == 64
typedef TCGv_i64 TCGv;
#define tcg_temp_new() tcg_temp_new_i64()
#define tcg_temp_free(v) tcg_temp_free_i64(v) // 添加64位的free
#define tcg_global_mem_new tcg_global_mem_new_i64
#define tcgv_tl_temp tcgv_i64_temp
#define tcg_gen_qemu_ld_tl tcg_gen_qemu_ld_i64
#define tcg_gen_qemu_st_tl tcg_gen_qemu_st_i64
#else
#error Unhandled TARGET_LONG_BITS value
#endif

修复编译警告

针对一些编译警告的修复如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
../accel/tcg/cpu-exec.c:586:10: warning: no previous prototype for ‘ijon_simple_hash’ [-Wmissing-prototypes]
586 | uint64_t ijon_simple_hash(uint64_t x) {
| ^~~~~~~~~~~~~~~~
// 在accel/tcg/cpu-exec.c实现的一些函数缺少声明
// 在qemuafl/common.h添加以下声明
uint64_t ijon_simple_hash(uint64_t x);
uint32_t ijon_hashint(uint32_t old, uint32_t val);
uint32_t ijon_hashstr(uint32_t old, char *val);
void ijon_max_variadic(uint32_t addr, ...);
void ijon_min_variadic(uint32_t addr, ...);
uint32_t ijon_strdist(char *a, char *b);
-----------
../accel/tcg/cpu-exec.c:1342:42: warning: implicit declaration of function ‘open_self_maps’; did you mean ‘free_self_maps’? [-Wimplicit-function-declaration]
1342 | if (getenv("AFL_QEMU_DEBUG_MAPS")) open_self_maps(env, 1);
| ^~~~~~~~~~~~~~
| free_self_maps
// 在accel/tcg/cpu-exec.c中patch代码使用到open_self_maps函数,该函数声明的头文件未包含。
// open_self_maps函数的实现位于linux-user/syscall.c,并且被声明为静态函数,因此我们需要把该函数的静态声明去除。
// linux-user/syscall.c修改内容:
-static int open_self_maps(CPUArchState *cpu_env, int fd)
+int open_self_maps(CPUArchState *cpu_env, int fd)
// 然后在qemuafl/common.h中添加函数声明
int open_self_maps(CPUArchState *cpu_env, int fd);
------------
./qemuafl/imported/snapshot-inl.h:110:13: warning: ‘afl_snapshot_clean’ defined but not used [-Wunused-function]
110 | static void afl_snapshot_clean(void) {
| ^~~~~~~~~~~~~~~~~~
./qemuafl/imported/snapshot-inl.h:98:12: warning: ‘afl_snapshot_do’ defined but not used [-Wunused-function]
98 | static int afl_snapshot_do(void) {
| ^~~~~~~~~~~~~~~
./qemuafl/imported/snapshot-inl.h:76:13: warning: ‘afl_snapshot_exclude_vmrange’ defined but not used [-Wunused-function]
76 | static void afl_snapshot_exclude_vmrange(void *start, void *end) {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 有几个函数声明的未使用,暂时注释掉
--------------
../linux-user/syscall.c: In function ‘do_execv’:
../linux-user/syscall.c:8649:23: warning: declaration of ‘p’ shadows a previous local [-Wshadow=local]
8649 | char *p, *q, *r;
| ^
../linux-user/syscall.c:8612:11: note: shadowed declaration is here
8612 | void *p;
| ^
../linux-user/syscall.c:8649:27: warning: declaration of ‘q’ shadows a previous local [-Wshadow=local]
8649 | char *p, *q, *r;
| ^
../linux-user/syscall.c:8611:12: note: shadowed declaration is here
8611 | char **q;
| ^
// 还有变量重复声明的问题,改个变量名就好

QEMU V10.1.3 修改方案

主要查看 QEMU V10.1.3 版本的三个配置文件,如下所示:

  1. accel/tcg/meson.build 代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if not have_tcg
subdir_done()
endif

tcg_ss = ss.source_set()

tcg_ss.add(files(
'cpu-exec.c',
'cpu-exec-common.c',
'tcg-runtime.c',
'tcg-runtime-gvec.c',
'tb-maint.c',
'tcg-all.c',
'tcg-stats.c',
'translate-all.c',
'translator.c',
))
if get_option('plugins')
tcg_ss.add(files('plugin-gen.c'))
endif

user_ss.add_all(tcg_ss)
system_ss.add_all(tcg_ss)

user_ss.add(files(
'user-exec.c',
'user-exec-stub.c',
))

system_ss.add(files(
'cputlb.c',
'icount-common.c',
'monitor.c',
'tcg-accel-ops.c',
'tcg-accel-ops-icount.c',
'tcg-accel-ops-mttcg.c',
'tcg-accel-ops-rr.c',
'watchpoint.c',
))

从上述代码可以看出,在当前架构,配置文件不区分 common_ss 通用代码和 tcg_specific_ss tcg 特定架构代码。而是分成 user_ss user 模式编译的代码和 system_ss system 模型编译的代码。

  1. accel/meson.build 代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
common_ss.add(files('accel-common.c'))
specific_ss.add(files('accel-target.c'))
system_ss.add(files('accel-system.c', 'accel-blocker.c', 'accel-qmp.c'))
user_ss.add(files('accel-user.c'))

subdir('tcg')
if have_system
subdir('hvf')
subdir('qtest')
subdir('kvm')
subdir('xen')
subdir('stubs')
endif

# qtest
system_ss.add(files('dummy-cpus.c'))
  1. meson.build 部分代码如下所示:
1
2
3
4
5
6
7
8
9
user_ss = ss.source_set()
......
libuser = static_library('user',
user_ss.all_sources() + genh,
c_args: ['-DCONFIG_USER_ONLY',
'-DCOMPILING_SYSTEM_VS_USER'],
include_directories: common_user_inc,
dependencies: user_ss.all_dependencies(),
build_by_default: false)

在当前架构中,accel/tcg/ 目录下的代码在第一次编译的时候都会被编译到 libuser 库中,无需考虑目标架构。但是该目录下有大量 AFL 补丁的代码,且 AFL 的代码有大量需要考虑目标架构。这就导致 AFL 的补丁代码无法直接迁移到该版本中。

不过,查看accel/meson.build的编译代码可以发现,仍然存在 specific_ss 配置,只需将 AFL 需要依赖目标架构的代码,迁移到 accel 目录下,然后把文件加入到 specific_ss 当中,即可在 QEMU V10.1.3 版本中成功编译 AFL 补丁版本。

QEMU V10.0.6 版本存在的 BUG

测试版本为 QEMU V10.0.6 linux-user 模式,目标架构为 mips。(V10.1.3 版本仍然存在)

成功编译 V10.0.6 版本的 qemuafl 后,开始尝试运行 afl fuzz。

前文曾提及 AFL 的 qemu 模式运行逻辑,默认情况下采用 forkserver 模式。在 fuzz 程序首次运行时,程序能正常运行,但在第二次执行到某个指令时,却会重新跳转回 main 函数,导致执行失败。

最终通过 QEMU 的 exec_tbtranslate_block 日志信息定位到了问题根源。

测试脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python3
import os
import sys
import struct
import time
import subprocess
import multiprocessing

# 模拟管道文件描述符,使用 os.pipe()
ctl_read, ctl_write = os.pipe() # 父→子 控制通道
st_read, st_write = os.pipe() # 子→父 状态通道

FORKSRV_FD = 198 # 模拟 AFL 使用的固定 FD
FUZZ_BIN = "/home/debian/fuzz/AFLplusplus/afl-qemu-trace"
MAGIC = 0x41464c01

def run_target(exec_bin):
pid = os.fork()
if pid == 0: # 子进程
# 先把管道 dup 到期望的 FD(模拟 forkserver 子进程环境)
os.dup2(ctl_read, FORKSRV_FD)
os.dup2(st_write, FORKSRV_FD + 1)
# 关闭不需要的文件描述符
os.close(ctl_write)
os.close(st_read)
os.execve(exec_bin, [exec_bin, "./usr/sbin/httpd_patch"] + sys.argv[1:], os.environ)


def parent_process():
run_target(FUZZ_BIN)
status = os.read(st_read, 4)
print(f"status = {status}")
reply = MAGIC ^ 0xFFFFFFFF
os.write(ctl_write, reply.to_bytes(4, "little"))
option = os.read(st_read, 4)
print(f"option = {option}")
map_size = os.read(st_read, 4)
print(f"map_size = {map_size}")
version = os.read(st_read, 4)
print(f"version = {version}")
while True:
was_killed = 0
os.write(ctl_write, was_killed.to_bytes(4, "little"))
child_pid = os.read(st_read, 4)
print(f"child pid = {int.from_bytes(child_pid, 'little')}")
status = os.read(st_read, 4)
print(f"status = {status}")
r = input("continue?")
if "q" in r:
break

if __name__ == "__main__":
parent_process()

调试命令如下所示:

1
QEMU_LOG_FILENAME="/tmp/debug.txt" QEMU_LOG="in_asm,out_asm" python3 afl-py.py

出现错误的流程大致如下:

  1. qemu 第一次 fork 出子进程,正常执行完整个流程。
  2. 父进程在第一个子进程执行的过程中,通过 afl_wait_tsl 函数,在父进程的内存空间中同步翻译指令块。这样,在下次 fork 出的子进程执行到同样的块时,就无需再翻译一次,可以节省代码执行时间。
  3. 第二个 fork 出的子进程按照父进程翻译的结果执行,在某个代码处开始出错。

经定位,错误代码位于 libuClibc 库的 strchr 函数中,关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.text:00038FA0 03 00 43 30                 andi    $v1, $v0, 3
.text:00038FA4 F9 FF 60 54 bnezl $v1, loc_38F8C
.text:00038FA8 00 00 43 90 lbu $v1, 0($v0)
.text:00038FAC 00 1A 05 00 sll $v1, $a1, 8
.text:00038FB0 25 18 65 00 or $v1, $a1
.text:00038FB4 00 5C 03 00 sll $t3, $v1, 16
.text:00038FB8 FE 7E 06 3C lui $a2, 0x7EFE
.text:00038FBC 01 81 0A 3C lui $t2, 0x8101
.text:00038FC0 25 58 63 01 or $t3, $v1
.text:00038FC4 FF FE C6 34 li $a2, 0x7EFEFEFF
.text:00038FC8 21 18 40 00 move $v1, $v0
.text:00038FCC 00 01 4A 35 li $t2, 0x81010100
.text:00038FD0
.text:00038FD0 loc_38FD0: # CODE XREF: index+7C↓j
.text:00038FD0 00 00 64 8C lw $a0, 0($v1)
.text:00038FD4
.text:00038FD4 loc_38FD4: # CODE XREF: index+D8↓j
.text:00038FD4 04 00 63 24 addiu $v1, 4
.text:00038FD8 26 38 8B 00 xor $a3, $a0, $t3
.text:00038FDC 21 40 E6 00 addu $t0, $a3, $a2
.text:00038FE0 21 10 86 00 addu $v0, $a0, $a2
.text:00038FE4 27 38 07 00 nor $a3, $zero, $a3
.text:00038FE8 27 20 04 00 nor $a0, $zero, $a0
.text:00038FEC 26 38 E8 00 xor $a3, $t0
.text:00038FF0 26 20 82 00 xor $a0, $v0
.text:00038FF4 25 20 E4 00 or $a0, $a3, $a0
.text:00038FF8 24 20 8A 00 and $a0, $t2
.text:00038FFC F4 FF 80 10 beqz $a0, loc_38FD0
.text:00039000 FC FF 69 24 addiu $t1, $v1, -4
.text:00039004 FC FF 64 90 lbu $a0, -4($v1)
.text:00039008 FD FF 62 24 addiu $v0, $v1, -3
.text:0003900C 03 00 85 14 bne $a0, $a1, loc_3901C
.text:00039010 FE FF 68 24 addiu $t0, $v1, -2
.text:00039014 08 00 E0 03 jr $ra
.text:00039018 21 10 20 01 move $v0, $t1

按照 QEMU 分块的逻辑,0x00038FAC - 0x0039000 算一个代码块。

但是在 target/mips/tcg/translate.c 文件的 mips_tr_translate_insn 函数中,有以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
static void mips_tr_translate_insn(DisasContextBase *dcbase, CPUState *cs)
{
......
/*
* End the TB on (most) page crossings.
* See mips_tr_init_disas_context about single-stepping a branch
* together with its delay slot.
*/
if (ctx->base.pc_next - ctx->page_start >= TARGET_PAGE_SIZE
&& !(tb_cflags(ctx->base.tb) & CF_SINGLE_STEP)) {
ctx->base.is_jmp = DISAS_TOO_MANY;
}
}

在上面的代码中 TARGET_PAGE_SIZE=4096=0x1000,通过注释也能知道,该部分代码是代码块按照页进行分割,一页的大小就是 0x1000

MIPS 指令集中存在 delay slot(延迟槽)机制,即在执行跳转指令之前,会先执行下一条指令。

1
2
3
4
5
6
7
8
9
比如以下指令
.text:00038FFC F4 FF 80 10 beqz $a0, loc_38FD0
.text:00039000 FC FF 69 24 addiu $t1, $v1, -4
实际执行过程如下:
$t1 = $v1-4
if $a0 == 0:
jump
else
no jump

但根据 QEMU 的分页机制,0x00038FAC - 0x0039000 块将会被分成:0x00038FAC - 0x0038FFC0x0039000 两个代码块。

不过,QEMU 的开发者也考虑到了这种情况,因此有以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void mips_tr_tb_stop(DisasContextBase *dcbase, CPUState *cs)
{
DisasContext *ctx = container_of(dcbase, DisasContext, base);

switch (ctx->base.is_jmp) {
case DISAS_STOP:
gen_save_pc(ctx->base.pc_next);
tcg_gen_lookup_and_goto_ptr();
break;
case DISAS_NEXT:
case DISAS_TOO_MANY:
save_cpu_state(ctx, 0);
gen_goto_tb(ctx, 0, ctx->base.pc_next);
break;
case DISAS_EXIT:
tcg_gen_exit_tb(NULL, 0);
break;
case DISAS_NORETURN:
break;
default:
g_assert_not_reached();
}
}

is_jmp == DISAS_TOO_MANY 时,首先保存上下文的 cpu 状态信息,然后设置当前块的下一跳为 delay slot 指令。

从这点看,QEMU 本身逻辑并无问题。分页是为了减少内存开销,同时也处理了被分开的代码块。

但是和 AFL 结合后,就会导致兼容性问题,产生 bug。下面将通过 QEMU 的日志信息,来展示该 BUG。

首先,第一个子进程在翻译代码块时的日志如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
IN(子进程):
0x2b506fac: sll v1,a1,0x8
0x2b506fb0: or v1,v1,a1
0x2b506fb4: sll t3,v1,0x10
0x2b506fb8: lui a2,0x7efe
0x2b506fbc: lui t2,0x8101
0x2b506fc0: or t3,t3,v1
0x2b506fc4: ori a2,a2,0xfeff
0x2b506fc8: move v1,v0
0x2b506fcc: ori t2,t2,0x100
0x2b506fd0: lw a0,0(v1)
0x2b506fd4: addiu v1,v1,4
0x2b506fd8: xor a3,a0,t3
0x2b506fdc: addu t0,a3,a2
0x2b506fe0: addu v0,a0,a2
0x2b506fe4: nor a3,zero,a3
0x2b506fe8: nor a0,zero,a0
0x2b506fec: xor a3,a3,t0
0x2b506ff0: xor a0,a0,v0
0x2b506ff4: or a0,a3,a0
0x2b506ff8: and a0,a0,t2
0x2b506ffc: beqz a0,0x2b506fd0

-- guest addr 0x000000002b506ffc
0x7f0cd4028655: 41 83 fc 01 cmpl $1, %r12d
0x7f0cd4028659: 1b db sbbl %ebx, %ebx
0x7f0cd402865b: f7 db negl %ebx
0x7f0cd402865d: 89 9d 24 1b 00 00 movl %ebx, 0x1b24(%rbp)
0x7f0cd4028663: c7 85 1c 1b 00 00 a2 10 movl $0x110a2, 0x1b1c(%rbp)
0x7f0cd402866b: 01 00
0x7f0cd402866d: c7 85 20 1b 00 00 d0 6f movl $0x2b506fd0, 0x1b20(%rbp)
0x7f0cd4028675: 50 2b
0x7f0cd4028677: c7 85 80 00 00 00 00 70 movl $0x2b507000, 0x80(%rbp)
0x7f0cd402867f: 50 2b
0x7f0cd4028681: 48 8b fd movq %rbp, %rdi
0x7f0cd4028684: ff 15 2e 00 00 00 callq *0x2e(%rip)
0x7f0cd402868a: ff e0 jmpq *%rax
0x7f0cd402868c: 48 8d 05 70 fe ff ff leaq -0x190(%rip), %rax
0x7f0cd4028693: e9 80 79 fd ff jmp 0x7f0cd4000018

IN(子进程):
0x2b507000: addiu t1,v1,-4

OUT: [size=120]
-- guest addr 0x000000002b507000 + tb prologue
0x7f0cd4028800: 8b 5d f8 movl -8(%rbp), %ebx
0x7f0cd4028803: 85 db testl %ebx, %ebx
0x7f0cd4028805: 0f 8c 58 00 00 00 jl 0x7f0cd4028863
0x7f0cd402880b: c6 45 fc 01 movb $1, -4(%rbp)
0x7f0cd402880f: 8b 5d 0c movl 0xc(%rbp), %ebx
0x7f0cd4028812: 83 c3 fc addl $-4, %ebx
0x7f0cd4028815: 89 5d 24 movl %ebx, 0x24(%rbp)
0x7f0cd4028818: c7 85 1c 1b 00 00 a2 00 movl $0xa2, 0x1b1c(%rbp)
0x7f0cd4028820: 00 00
0x7f0cd4028822: 8b 9d 24 1b 00 00 movl 0x1b24(%rbp), %ebx
0x7f0cd4028828: 85 db testl %ebx, %ebx
0x7f0cd402882a: 0f 85 1e 00 00 00 jne 0x7f0cd402884e
0x7f0cd4028830: 66 66 90 nop
0x7f0cd4028833: e9 00 00 00 00 jmp 0x7f0cd4028838
0x7f0cd4028838: c7 85 80 00 00 00 04 70 movl $0x2b507004, 0x80(%rbp)
0x7f0cd4028840: 50 2b
0x7f0cd4028842: 48 8d 05 f8 fe ff ff leaq -0x108(%rip), %rax
0x7f0cd4028849: e9 ca 77 fd ff jmp 0x7f0cd4000018
0x7f0cd402884e: c7 85 80 00 00 00 d0 6f movl $0x2b506fd0, 0x80(%rbp)
0x7f0cd4028856: 50 2b
0x7f0cd4028858: 48 8b fd movq %rbp, %rdi
0x7f0cd402885b: ff 15 0f 00 00 00 callq *0xf(%rip)
0x7f0cd4028861: ff e0 jmpq *%rax
0x7f0cd4028863: 48 8d 05 d9 fe ff ff leaq -0x127(%rip), %rax
0x7f0cd402886a: e9 a9 77 fd ff jmp 0x7f0cd4000018

观察上述指令发现,QEMU 翻译的跳转目标地址出现了偏差:movl $0x402ac0, 0x80(%rbp)0x402ac0 地址为 httpd 程序的 _start 函数起始地址。

经过详细分析与调试,明确了该 Bug 的成因。

将原本的分支指令块设为 TB_A,把被分片的 delay slot 指令块设为 TB_B

当 QEMU 翻译完 TB_A 代码块后,将会执行 mips_tr_tb_stop 函数,然后调用 save_cpu_state 函数:代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline void save_cpu_state(DisasContext *ctx, int do_save_pc)
{
LOG_DISAS("hflags %08x saved %08x\n", ctx->hflags, ctx->saved_hflags);
if (do_save_pc && ctx->base.pc_next != ctx->saved_pc) {
gen_save_pc(ctx->base.pc_next);
ctx->saved_pc = ctx->base.pc_next;
}
if (ctx->hflags != ctx->saved_hflags) {
tcg_gen_movi_i32(hflags, ctx->hflags);
ctx->saved_hflags = ctx->hflags;
switch (ctx->hflags & MIPS_HFLAG_BMASK_BASE) {
case MIPS_HFLAG_BR:
break;
case MIPS_HFLAG_BC:
case MIPS_HFLAG_BL:
case MIPS_HFLAG_B:
tcg_gen_movi_tl(btarget, ctx->btarget);
break;
}
}
}

因为在 mips_tr_tb_stop 函数中调用的是 save_cpu_state(ctx, 0);,因此 do_save_pc=0。最终只执行了三句指令:

1
2
3
tcg_gen_movi_i32(hflags, ctx->hflags);
ctx->saved_hflags = ctx->hflags;
tcg_gen_movi_tl(btarget, ctx->btarget);

这三句指令可以对应到之前 QEMU 日志中的指令,如下所示:

1
2
3
4
5
6
# 设置hflags
tcg_gen_movi_i32(hflags, ctx->hflags);
-> 0x7f0cd4028663: movl $0x110a2, 0x1b1c(%rbp)
# 设置btarget
tcg_gen_movi_tl(btarget, ctx->btarget);
-> 0x7f0cd402866d: movl $0x2b506fd0, 0x1b20(%rbp)

当前代码块翻译完成后,接下来翻译 TB_B 块。在翻译块的开头调用 mips_tr_init_disas_context 函数进行上下文变量初始化,将会调用 restore_cpu_state 函数,恢复 btarget 值,相关代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void restore_cpu_state(CPUMIPSState *env, DisasContext *ctx)
{
ctx->saved_hflags = ctx->hflags;
switch (ctx->hflags & MIPS_HFLAG_BMASK_BASE) {
case MIPS_HFLAG_BR:
break;
case MIPS_HFLAG_BC:
case MIPS_HFLAG_BL:
case MIPS_HFLAG_B:
ctx->btarget = env->btarget;
break;
}
}

上述代码是导致本次 Bug 的另一个核心点,下文将详细说明。在初始化完上下文信息后,将会调用 mips_tr_translate_insn 翻译代码,首先翻译 add 指令。翻译完成后,因为 is_slot=True,所以将会调用 gen_branch(ctx, insn_bytes); 函数生成分支跳转代码。关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void mips_tr_translate_insn(DisasContextBase *dcbase, CPUState *cs)
{
......
if (is_slot) {
gen_branch(ctx, insn_bytes);
}
......
}
static void gen_branch(DisasContext *ctx, int insn_bytes)
{
if (ctx->hflags & MIPS_HFLAG_BMASK) {
int proc_hflags = ctx->hflags & MIPS_HFLAG_BMASK;
/* Branches completion */
clear_branch_hflags(ctx);
ctx->base.is_jmp = DISAS_NORETURN;
switch (proc_hflags & MIPS_HFLAG_BMASK_BASE) {
......
case MIPS_HFLAG_BC:
/* Conditional branch */
{
TCGLabel *l1 = gen_new_label();

tcg_gen_brcondi_tl(TCG_COND_NE, bcond, 0, l1);
gen_goto_tb(ctx, 1, ctx->base.pc_next + insn_bytes);
gen_set_label(l1);
gen_goto_tb(ctx, 0, ctx->btarget);
}
break;
......
}

该部分代码可以和上面 QEMU 日志信息进行一一对应,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tcg_gen_brcondi_tl(TCG_COND_NE, bcond, 0, l1);
->
0x7f0cd4028822: movl 0x1b24(%rbp), %ebx
0x7f0cd4028828: testl %ebx, %ebx
0x7f0cd402882a: jne 0x7f0cd402884e (l1地址)
gen_goto_tb(ctx, 1, ctx->base.pc_next + insn_bytes);
->
0x7f0cd4028833: jmp 0x7f0cd4028838
0x7f0cd4028838: movl $0x2b507004, 0x80(%rbp)
gen_set_label(l1);
gen_goto_tb(ctx, 0, ctx->btarget);
->
l1:
0x7f0cd402884e: c7 85 80 00 00 00 d0 6f movl $0x2b506fd0, 0x80(%rbp)

至此,相关流程已梳理完毕。该 Bug 情况总体概括如下:AFL 在翻译 TB_B 代码块时,由于 ctx->btarget 的值错误地等于 0x402ac0,导致翻译出来的指令为:movl 0x402ac0, 0x80(%rbp)。在代码执行到该分支后,将会错误地跳转回程序的 _start 函数,最终导致程序崩溃。

总体梳理一下该 BUG 的成因:

  1. QMEU 会根据 0x1000 大小对代码块进行分片。这可能会导致分支跳转指令和其 delay slot 指令被分成两块:TB_ATB_B
  2. QEMU 考虑到分片会影响 delay slot 的情况,因此在翻译完 TB_A 后,会调用 save_cpu_state 函数保存跳转地址信息。
  3. save_cpu_state 函数保存 btarget 的方案是翻译成 TCG 代码(movl $0x2b506fd0, 0x1b20(%rbp))。
  4. restore_cpu_state 函数恢复 btarget 的方案是从 env->btarget 上下文中获取。

上述的 3, 4 两点就产生了冲突,最终导致 BUG 的产生。

在原版的 QEMU 中,因为性能考虑,不会一次性把所有代码都翻译成 TCG 指令。而是运行到哪,翻译到哪。因此在翻译完 TB_A 指令后,将会执行 TB_A 指令。TB_A 翻译出的 TCG 指令最终会跳转到 TB_B 地址,因此下一步将会翻译 TB_B 指令。由于已经执行了 TB_A TCG 指令中的 movl $0x2b506fd0, 0x1b20(%rbp) 指令,因此 env->btarget 已经被成功设置成正确的 btarget 地址了。所以在后面翻译 TB_B 指令的流程中没有出错。

在 qemuafl 中,第一个子进程的翻译过程就是按照 QEMU 原版的逻辑来运行,因此不会出错。但是,随后父进程将会跟着开始翻译代码块,却不执行,这就导致 env->btarget 无法被正确设置,最终代码出错,程序崩溃。

简单来说,原版 QEMU 的流程为:翻译TB_A -> 执行TB_A -> 翻译TB_B -> 执行TB_B
qemuafl 父进程的流程为:翻译TB_A -> 翻译TB_B -> ...

随后对原版 qemuafl(QEMU V5 版本)进行的简要分析显示,该 Bug 仍然存在,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
OUT: [size=114]
-- guest addr 0x3fd87000 + tb prologue
0x7fe6dc01ce9b: 44 8b e3 movl %ebx, %r12d
0x7fe6dc01ce9e: 41 83 e4 01 andl $1, %r12d
0x7fe6dc01cea2: 44 8b ad 9c 1a 00 00 movl 0x1a9c(%rbp), %r13d
0x7fe6dc01cea9: 41 81 e5 ff fb ff ff andl $0xfffffbff, %r13d
0x7fe6dc01ceb0: 41 c1 e4 0a shll $0xa, %r12d
0x7fe6dc01c240: 8b 5d f8 movl -8(%rbp), %ebx
0x7fe6dc01ceb4: 45 0b ec orl %r12d, %r13d
0x7fe6dc01c243: 85 db testl %ebx, %ebx
0x7fe6dc01ceb7: 44 89 ad 9c 1a 00 00 movl %r13d, 0x1a9c(%rbp)
0x7fe6dc01c245: 0f 8c 5b 00 00 00 jl 0x7fe6dc01c2a6
0x7fe6dc01cebe: 83 e3 fe andl $0xfffffffe, %ebx
0x7fe6dc01c24b: 8b 5d 0c movl 0xc(%rbp), %ebx
0x7fe6dc01cec1: 89 9d 80 00 00 00 movl %ebx, 0x80(%rbp)
0x7fe6dc01c24e: 83 c3 fc addl $-4, %ebx
0x7fe6dc01cec7: 48 8b fd movq %rbp, %rdi
0x7fe6dc01c251: 89 5d 24 movl %ebx, 0x24(%rbp)
0x7fe6dc01ceca: ff 15 10 00 00 00 callq *0x10(%rip)
0x7fe6dc01c254: c7 85 9c 1a 00 00 a2 00 movl $0xa2, 0x1a9c(%rbp)
0x7fe6dc01ced0: ff e0 jmpq *%rax
0x7fe6dc01c25c: 00 00
0x7fe6dc01ced2: 48 8d 05 2a ff ff ff leaq -0xd6(%rip), %rax
0x7fe6dc01c25e: 8b 9d a4 1a 00 00 movl 0x1aa4(%rbp), %ebx
0x7fe6dc01ced9: e9 3a 31 fe ff jmp 0x7fe6dc000018
0x7fe6dc01c264: 85 db testl %ebx, %ebx
-- tb slow paths + alignment
0x7fe6dc01c266: 0f 85 1e 00 00 00 jne 0x7fe6dc01c28a
0x7fe6dc01c26c: 66 66 90 nop
0x7fe6dc01c26f: e9 00 00 00 00 jmp 0x7fe6dc01c274
0x7fe6dc01c274: c7 85 80 00 00 00 04 70 movl $0x3fd87004, 0x80(%rbp)
0x7fe6dc01c27c: d8 3f
0x7fe6dc01cede: 90 nop
0x7fe6dc01c27e: 48 8d 05 3c ff ff ff leaq -0xc4(%rip), %rax
0x7fe6dc01cedf: 90 nop
0x7fe6dc01c285: e9 8e 3d fe ff jmp 0x7fe6dc000018
data: [size=8]
0x7fe6dc01c28a: 90 nop
0x7fe6dc01cee0: .quad 0x55ad16a506f0
0x7fe6dc01c28b: e9 00 00 00 00 jmp 0x7fe6dc01c290

0x7fe6dc01c290: c7 85 80 00 00 00 c0 2a movl $0x402ac0, 0x80(%rbp)
0x7fe6dc01c298: 40 00
0x7fe6dc01c29a: 48 8d 05 1f ff ff ff leaq -0xe1(%rip), %rax
0x7fe6dc01c2a1: e9 72 3d fe ff jmp 0x7fe6dc000018
0x7fe6dc01c2a6: 48 8d 05 16 ff ff ff leaq -0xea(%rip), %rax
0x7fe6dc01c2ad: e9 66 3d fe ff jmp 0x7fe6dc000018

0x7fe6dc01c290 地址的指令仍然出错,至于为何未能触发该 Bug,需要进一步调试分析,感兴趣的读者可自行探索。

修复方案

  1. 不把 delay slot 进行分片,patch 代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
/*
* End the TB on (most) page crossings.
* See mips_tr_init_disas_context about single-stepping a branch
* together with its delay slot.
*/
if (ctx->base.pc_next - ctx->page_start >= TARGET_PAGE_SIZE
&& !(tb_cflags(ctx->base.tb) & CF_SINGLE_STEP)
&& !(ctx->hflags & MIPS_HFLAG_BMASK) // patch代码
) {
ctx->base.is_jmp = DISAS_TOO_MANY;
}

该修复方案优点在于快捷简单,缺点是可能会影响代码分片,从而对 QEMU 执行效率产生一定影响。

  1. 修改gen_branch函数的逻辑,如下所示:
1
2
3
4
gen_goto_tb(ctx, 0, ctx->btarget);
修改成->
tcg_gen_mov_tl(cpu_PC, btarget);
tcg_gen_lookup_and_goto_ptr();

该修复方案同样快捷简单,把所有分支跳转的btarget都修改成从内存中获取。缺点是性能开销大,这步骤需要进行内存寻址操作,性能开销远大于硬编码。mips代码中肯定会存在大量分支跳转指令,这会大大影响的程序的执行效率。

  1. 只针对该BUG,修改gen_branch函数的逻辑,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
target/mips/tcg/translate.h 中:
typedef struct DisasContext {
DisasContextBase base;
// ... 原有字段 ...
bool mi;
int gi;
// 新增字段
bool btarget_from_env;
} DisasContext;

target/mips/tcg/translate.c
static void mips_tr_init_disas_context(DisasContextBase *dcbase, CPUState *cs)
{
// ... 原有代码 ...
restore_cpu_state(env, ctx); // 这里 ctx->btarget 被赋值为 env->btarget

// 新增代码:如果当前处于 Branch Mask 状态,说明是在 delay slot 中开始的 TB
ctx->btarget_from_env = (ctx->hflags & MIPS_HFLAG_BMASK) != 0;

// ... 原有代码 ...
}
...
static void gen_branch(DisasContext *ctx, int insn_bytes)
{
if (ctx->hflags & MIPS_HFLAG_BMASK) {
int proc_hflags = ctx->hflags & MIPS_HFLAG_BMASK;
// ... 原有代码 ...
switch (proc_hflags & MIPS_HFLAG_BMASK_BASE) {
// ...
case MIPS_HFLAG_B:
/* unconditional branch */
if (proc_hflags & MIPS_HFLAG_BX) {
tcg_gen_xori_i32(hflags, hflags, MIPS_HFLAG_M16);
}
// 修改开始
if (ctx->btarget_from_env) {
tcg_gen_mov_tl(cpu_PC, btarget);
tcg_gen_lookup_and_goto_ptr();
} else {
gen_goto_tb(ctx, 0, ctx->btarget);
}
// 修改结束
break;
case MIPS_HFLAG_BL:
/* blikely taken case */
// 修改开始
if (ctx->btarget_from_env) {
tcg_gen_mov_tl(cpu_PC, btarget);
tcg_gen_lookup_and_goto_ptr();
} else {
gen_goto_tb(ctx, 0, ctx->btarget);
}
// 修改结束
break;
case MIPS_HFLAG_BC:
/* Conditional branch */
{
TCGLabel *l1 = gen_new_label();

tcg_gen_brcondi_tl(TCG_COND_NE, bcond, 0, l1);
gen_goto_tb(ctx, 1, ctx->base.pc_next + insn_bytes);
gen_set_label(l1);
// 修改开始
if (ctx->btarget_from_env) {
tcg_gen_mov_tl(cpu_PC, btarget);
tcg_gen_lookup_and_goto_ptr();
} else {
gen_goto_tb(ctx, 0, ctx->btarget);
}
// 修改结束
}
break;
// ... MIPS_HFLAG_BR 本身已经是动态跳转了,无需修改 ...
}
}
}

该方案由AI生成,修改起来稍微复杂一点,但是性能损失小。

QEMU v10 适配 AFL:架构变更与 MIPS 延迟槽 Bug 分析

https://nobb.site/2025/11/13/fuzz4/

Author

Hcamael

Posted on

2025-11-13

Updated on

2025-12-16

Licensed under