プログラムを作成することで問題を解決する、25年の経験を持つプログラマです。
学生時代には MS-DOS / Windows 向けフリーソフトを開発していました。
C言語で数百から数千行程度のソフトウェアを開発することが多いです。
性格:必要なものを、ターゲットに特化した内容で作成するのが特技です。
「企業内でどのようにOSSの活動をやっているか」というテーマで話をしてほしいということで講演依頼を受けているので、 syzbot の話に行く前にちょこっとだけ触れておきます。
・・・ということで、残りの時間は syzbot をめぐる苦労話で埋め尽くします。
ファジングテストにより不具合を発見して不具合を再現するための再現プログラムを自動生成するシステムである syzkaller を用いた、バグトラッキングシステムです。
https://github.com/google/syzkaller/blob/master/docs/syzbot.md
(2020年09月11日23時00分時点の画像です)
(2020年09月11日23時00分時点の画像です)
(2020年09月11日23時00分時点の画像です)第1弾は https://I-love.SAKURA.ne.jp/The_SYZBOT_CTF.html です。
今日は、第1弾のその後の出来事の中から話をします。
簡単な不具合から難解な不具合まで、累計発生回数の多い不具合/長期間放置されている不具合を中心に関わってきました。
これは、何のメッセージも出力せずに無応答になるという不具合の再現プログラムです。
// https://syzkaller.appspot.com/bug?id=0b210638616bb68109e9642158d4c0072770ae1c
// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
static long syz_open_dev(volatile long a0, volatile long a1, volatile long a2)
{
if (a0 == 0xc || a0 == 0xb) {
char buf[128];
sprintf(buf, "/dev/%s/%d:%d", a0 == 0xc ? "char" : "block", (uint8_t)a1,
(uint8_t)a2);
return open(buf, O_RDWR, 0);
} else {
char buf[1024];
char* hash;
strncpy(buf, (char*)a0, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = 0;
while ((hash = strchr(buf, '#'))) {
*hash = '0' + (char)(a1 % 10);
a1 /= 10;
}
return open(buf, a2, 0);
}
}
uint64_t r[2] = {0xffffffffffffffff, 0xffffffffffffffff};
int main(void)
{
syscall(__NR_mmap, 0x1ffff000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x20000000ul, 0x1000000ul, 7ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x21000000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
intptr_t res = 0;
memcpy((void*)0x20000080, "/dev/nbd#\000", 10);
res = -1;
res = syz_open_dev(0x20000080, 0, 0);
if (res != -1)
r[0] = res;
res = syscall(__NR_socket, 0x1eul, 2ul, 0);
if (res != -1)
r[1] = res;
syscall(__NR_ioctl, r[0], 0xab00, r[1]);
syscall(__NR_ioctl, r[0], 0xab03, 0);
return 0;
}
処理内容を理解しやすいように書き直した再現プログラムです。
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/nbd.h>
int main(int argc, char *argv[])
{
const int fd = open("/dev/nbd0", 3);
ioctl(fd, NBD_SET_SOCK, socket(PF_TIPC, SOCK_DGRAM, 0));
ioctl(fd, NBD_DO_IT, 0);
return 0;
}
ちょっと修正すると、類似の不具合の再現プログラムに化けました。
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/nbd.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
const int fd = open("/dev/nbd0", 3);
alarm(5);
ioctl(fd, NBD_SET_SOCK, socket(PF_TIPC, SOCK_DGRAM, 0));
ioctl(fd, NBD_DO_IT, 0); /* To be interrupted by SIGALRM. */
return 0;
}
この不具合は以下のパッチにより修正されました。
さらに修正すると、 syzbot では不具合とは判断されない種類の別の不具合の再現プログラムに化けました。
#include <sys/socket.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int fds[2] = { -1, -1 };
socketpair(PF_TIPC, SOCK_STREAM /* or SOCK_DGRAM */, 0, fds);
if (fork() == 0)
_exit(read(fds[0], NULL, 1));
shutdown(fds[0], SHUT_RDWR); /* This must make read() return. */
wait(NULL); /* To be woken up by _exit(). */
return 0;
}
この不具合は以下のパッチにより修正されました。
これは、ワークキューの処理でロックアップが発生するという不具合の再現プログラムです。
// https://syzkaller.appspot.com/bug?id=17a06459ecad660a3d00c3e41a4b1a7be6bca19e
// autogenerated by syzkaller (http://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <fcntl.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/if_tun.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <net/if_arp.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <unistd.h>
__attribute__((noreturn)) static void doexit(int status)
{
volatile unsigned i;
syscall(__NR_exit_group, status);
for (i = 0;; i++) {
}
}
#include <setjmp.h>
#include <signal.h>
#include <stdint.h>
#include <string.h>
#include <string.h>
const int kFailStatus = 67;
const int kRetryStatus = 69;
static void fail(const char* msg, ...)
{
int e = errno;
va_list args;
va_start(args, msg);
vfprintf(stderr, msg, args);
va_end(args);
fprintf(stderr, " (errno %d)\n", e);
doexit((e == ENOMEM || e == EAGAIN) ? kRetryStatus : kFailStatus);
}
static __thread int skip_segv;
static __thread jmp_buf segv_env;
static void segv_handler(int sig, siginfo_t* info, void* uctx)
{
uintptr_t addr = (uintptr_t)info->si_addr;
const uintptr_t prog_start = 1 << 20;
const uintptr_t prog_end = 100 << 20;
if (__atomic_load_n(&skip_segv, __ATOMIC_RELAXED) &&
(addr < prog_start || addr > prog_end)) {
_longjmp(segv_env, 1);
}
doexit(sig);
for (;;) {
}
}
static void install_segv_handler()
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
syscall(SYS_rt_sigaction, 0x20, &sa, NULL, 8);
syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8);
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = segv_handler;
sa.sa_flags = SA_NODEFER | SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
}
#define NONFAILING(...) \
{ \
__atomic_fetch_add(&skip_segv, 1, __ATOMIC_SEQ_CST); \
if (_setjmp(segv_env) == 0) { \
__VA_ARGS__; \
} \
__atomic_fetch_sub(&skip_segv, 1, __ATOMIC_SEQ_CST); \
}
static void vsnprintf_check(char* str, size_t size, const char* format,
va_list args)
{
int rv;
rv = vsnprintf(str, size, format, args);
if (rv < 0)
fail("tun: snprintf failed");
if ((size_t)rv >= size)
fail("tun: string '%s...' doesn't fit into buffer", str);
}
static void snprintf_check(char* str, size_t size, const char* format,
...)
{
va_list args;
va_start(args, format);
vsnprintf_check(str, size, format, args);
va_end(args);
}
#define COMMAND_MAX_LEN 128
#define PATH_PREFIX \
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin "
#define PATH_PREFIX_LEN (sizeof(PATH_PREFIX) - 1)
static void execute_command(const char* format, ...)
{
va_list args;
char command[PATH_PREFIX_LEN + COMMAND_MAX_LEN];
int rv;
va_start(args, format);
memcpy(command, PATH_PREFIX, PATH_PREFIX_LEN);
vsnprintf_check(command + PATH_PREFIX_LEN, COMMAND_MAX_LEN, format,
args);
rv = system(command);
if (rv != 0)
fail("tun: command \"%s\" failed with code %d", &command[0], rv);
va_end(args);
}
static int tunfd = -1;
static int tun_frags_enabled;
#define SYZ_TUN_MAX_PACKET_SIZE 1000
#define MAX_PIDS 32
#define ADDR_MAX_LEN 32
#define LOCAL_MAC "aa:aa:aa:aa:aa:%02hx"
#define REMOTE_MAC "bb:bb:bb:bb:bb:%02hx"
#define LOCAL_IPV4 "172.20.%d.170"
#define REMOTE_IPV4 "172.20.%d.187"
#define LOCAL_IPV6 "fe80::%02hxaa"
#define REMOTE_IPV6 "fe80::%02hxbb"
#define IFF_NAPI 0x0010
#define IFF_NAPI_FRAGS 0x0020
static void initialize_tun(uint64_t pid)
{
if (pid >= MAX_PIDS)
fail("tun: no more than %d executors", MAX_PIDS);
int id = pid;
tunfd = open("/dev/net/tun", O_RDWR | O_NONBLOCK);
if (tunfd == -1) {
printf(
"tun: can't open /dev/net/tun: please enable CONFIG_TUN=y\n");
printf("otherwise fuzzing or reproducing might not work as "
"intended\n");
return;
}
char iface[IFNAMSIZ];
snprintf_check(iface, sizeof(iface), "syz%d", id);
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, iface, IFNAMSIZ);
ifr.ifr_flags = IFF_TAP | IFF_NO_PI | IFF_NAPI | IFF_NAPI_FRAGS;
if (ioctl(tunfd, TUNSETIFF, (void*)&ifr) < 0) {
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
if (ioctl(tunfd, TUNSETIFF, (void*)&ifr) < 0)
fail("tun: ioctl(TUNSETIFF) failed");
}
if (ioctl(tunfd, TUNGETIFF, (void*)&ifr) < 0)
fail("tun: ioctl(TUNGETIFF) failed");
tun_frags_enabled = (ifr.ifr_flags & IFF_NAPI_FRAGS) != 0;
char local_mac[ADDR_MAX_LEN];
snprintf_check(local_mac, sizeof(local_mac), LOCAL_MAC, id);
char remote_mac[ADDR_MAX_LEN];
snprintf_check(remote_mac, sizeof(remote_mac), REMOTE_MAC, id);
char local_ipv4[ADDR_MAX_LEN];
snprintf_check(local_ipv4, sizeof(local_ipv4), LOCAL_IPV4, id);
char remote_ipv4[ADDR_MAX_LEN];
snprintf_check(remote_ipv4, sizeof(remote_ipv4), REMOTE_IPV4, id);
char local_ipv6[ADDR_MAX_LEN];
snprintf_check(local_ipv6, sizeof(local_ipv6), LOCAL_IPV6, id);
char remote_ipv6[ADDR_MAX_LEN];
snprintf_check(remote_ipv6, sizeof(remote_ipv6), REMOTE_IPV6, id);
execute_command("sysctl -w net.ipv6.conf.%s.accept_dad=0", iface);
execute_command("sysctl -w net.ipv6.conf.%s.router_solicitations=0",
iface);
execute_command("ip link set dev %s address %s", iface, local_mac);
execute_command("ip addr add %s/24 dev %s", local_ipv4, iface);
execute_command("ip -6 addr add %s/120 dev %s", local_ipv6, iface);
execute_command("ip neigh add %s lladdr %s dev %s nud permanent",
remote_ipv4, remote_mac, iface);
execute_command("ip -6 neigh add %s lladdr %s dev %s nud permanent",
remote_ipv6, remote_mac, iface);
execute_command("ip link set dev %s up", iface);
}
static void setup_tun(uint64_t pid, bool enable_tun)
{
if (enable_tun)
initialize_tun(pid);
}
static uintptr_t syz_open_pts(uintptr_t a0, uintptr_t a1)
{
int ptyno = 0;
if (ioctl(a0, TIOCGPTN, &ptyno))
return -1;
char buf[128];
sprintf(buf, "/dev/pts/%d", ptyno);
return open(buf, a1, 0);
}
#ifndef __NR_memfd_create
#define __NR_memfd_create 319
#endif
long r[4];
void* thr(void* arg)
{
switch ((long)arg) {
case 0:
syscall(__NR_mmap, 0x20000000ul, 0xfdf000ul, 0x3ul, 0x32ul,
0xfffffffffffffffful, 0x0ul);
break;
case 1:
NONFAILING(memcpy((void*)0x20960000, "/dev/ptmx", 10));
r[0] = syscall(__NR_openat, 0xffffffffffffff9cul, 0x20960000ul,
0x0ul, 0x0ul);
break;
case 2:
NONFAILING(memcpy((void*)0x20fdbff7, "^keyring", 9));
r[1] = syscall(__NR_memfd_create, 0x20fdbff7ul, 0x2ul);
break;
case 3:
NONFAILING(memcpy((void*)0x20375000, "/dev/autofs", 12));
r[2] = syscall(__NR_openat, 0xffffffffffffff9cul, 0x20375000ul,
0x4000ul, 0x0ul);
break;
case 4:
NONFAILING(*(uint32_t*)0x20fdb000 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x20fdb004 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x2070cffc = (uint32_t)0x8);
syscall(__NR_getsockopt, r[2], 0x84ul, 0x6dul, 0x20fdb000ul,
0x2070cffcul);
break;
case 5:
NONFAILING(*(uint16_t*)0x20fd6000 = (uint16_t)0x0);
NONFAILING(*(uint16_t*)0x20fd6002 = (uint16_t)0x0);
NONFAILING(*(uint16_t*)0x20fd6004 = (uint16_t)0x0);
NONFAILING(*(uint16_t*)0x20fd6006 = (uint16_t)0x0);
NONFAILING(*(uint8_t*)0x20fd6008 = (uint8_t)0x0);
NONFAILING(*(uint8_t*)0x20fd6009 = (uint8_t)0x3fd);
NONFAILING(*(uint8_t*)0x20fd600a = (uint8_t)0x0);
NONFAILING(*(uint8_t*)0x20fd600b = (uint8_t)0x0);
NONFAILING(*(uint32_t*)0x20fd600c = (uint32_t)0xffffffff);
NONFAILING(*(uint8_t*)0x20fd6010 = (uint8_t)0xfff);
syscall(__NR_ioctl, r[0], 0x5402ul, 0x20fd6000ul);
break;
case 6:
NONFAILING(*(uint32_t*)0x20fdcf88 = (uint32_t)0x5);
NONFAILING(*(uint32_t*)0x20fdcf8c = (uint32_t)0x78);
NONFAILING(*(uint8_t*)0x20fdcf90 = (uint8_t)0xffffffffffffff81);
NONFAILING(*(uint8_t*)0x20fdcf91 = (uint8_t)0x1f);
NONFAILING(*(uint8_t*)0x20fdcf92 = (uint8_t)0x100000000);
NONFAILING(*(uint8_t*)0x20fdcf93 = (uint8_t)0xffffffff);
NONFAILING(*(uint32_t*)0x20fdcf94 = (uint32_t)0x0);
NONFAILING(*(uint64_t*)0x20fdcf98 = (uint64_t)0xfff);
NONFAILING(*(uint64_t*)0x20fdcfa0 = (uint64_t)0x10000);
NONFAILING(*(uint64_t*)0x20fdcfa8 = (uint64_t)0x2);
NONFAILING(*(uint8_t*)0x20fdcfb0 = (uint8_t)0x4);
NONFAILING(*(uint8_t*)0x20fdcfb1 = (uint8_t)0x100);
NONFAILING(*(uint8_t*)0x20fdcfb2 = (uint8_t)0x9);
NONFAILING(*(uint8_t*)0x20fdcfb3 = (uint8_t)0x4);
NONFAILING(*(uint32_t*)0x20fdcfb4 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x20fdcfb8 = (uint32_t)0x80);
NONFAILING(*(uint32_t*)0x20fdcfbc = (uint32_t)0x2);
NONFAILING(*(uint64_t*)0x20fdcfc0 = (uint64_t)0x3);
NONFAILING(*(uint64_t*)0x20fdcfc8 = (uint64_t)0x6);
NONFAILING(*(uint64_t*)0x20fdcfd0 = (uint64_t)0x2);
NONFAILING(*(uint64_t*)0x20fdcfd8 = (uint64_t)0x4);
NONFAILING(*(uint64_t*)0x20fdcfe0 = (uint64_t)0x24b);
NONFAILING(*(uint32_t*)0x20fdcfe8 = (uint32_t)0x7);
NONFAILING(*(uint64_t*)0x20fdcff0 = (uint64_t)0x80000001);
NONFAILING(*(uint32_t*)0x20fdcff8 = (uint32_t)0x8);
NONFAILING(*(uint16_t*)0x20fdcffc = (uint16_t)0xff);
NONFAILING(*(uint16_t*)0x20fdcffe = (uint16_t)0x0);
syscall(__NR_perf_event_open, 0x20fdcf88ul, 0x0ul, 0x2ul, r[1],
0x5ul);
break;
case 7:
NONFAILING(*(uint32_t*)0x203b9fdc = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x203b9fe0 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x203b9fe4 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x203b9fe8 = (uint32_t)0x0);
NONFAILING(*(uint8_t*)0x203b9fec = (uint8_t)0x0);
NONFAILING(*(uint8_t*)0x203b9fed = (uint8_t)0x0);
NONFAILING(*(uint8_t*)0x203b9fee = (uint8_t)0x0);
NONFAILING(*(uint8_t*)0x203b9fef = (uint8_t)0x0);
NONFAILING(*(uint32_t*)0x203b9ff0 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x203b9ff4 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x203b9ff8 = (uint32_t)0x0);
NONFAILING(*(uint32_t*)0x203b9ffc = (uint32_t)0x0);
syscall(__NR_ioctl, r[0], 0x40045431ul, 0x203b9fdcul);
break;
case 8:
r[3] = syz_open_pts(r[0], 0x0ul);
break;
case 9:
NONFAILING(*(uint64_t*)0x20fd6000 = (uint64_t)0x200c1f45);
NONFAILING(*(uint64_t*)0x20fd6008 = (uint64_t)0xbb);
NONFAILING(*(uint64_t*)0x20fd6010 = (uint64_t)0x20227fd0);
NONFAILING(*(uint64_t*)0x20fd6018 = (uint64_t)0x30);
NONFAILING(*(uint64_t*)0x20fd6020 = (uint64_t)0x20063000);
NONFAILING(*(uint64_t*)0x20fd6028 = (uint64_t)0x7b);
NONFAILING(*(uint64_t*)0x20fd6030 = (uint64_t)0x20313f29);
NONFAILING(*(uint64_t*)0x20fd6038 = (uint64_t)0xd7);
syscall(__NR_readv, r[3], 0x20fd6000ul, 0x4ul);
break;
case 10:
syscall(__NR_ioctl, r[0], 0x540aul, 0x2ul);
break;
case 11:
syscall(__NR_ioctl, r[3], 0x541bul, 0x20f76ffful);
break;
}
return 0;
}
void loop()
{
long i;
pthread_t th[24];
memset(r, -1, sizeof(r));
for (i = 0; i < 12; i++) {
pthread_create(&th[i], 0, thr, (void*)i);
usleep(rand() % 10000);
}
usleep(rand() % 100000);
}
int main()
{
install_segv_handler();
setup_tun(0, true);
loop();
return 0;
}
人力で解析を行って必要最小限の処理だけを抽出した再現プログラムです。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <termios.h>
static void *thr(void *arg)
{
char c;
read(*(int *) arg, &c, 1);
return 0;
}
int main(int argc, char *argv[])
{
char buf[sizeof(struct termios) + 64] = { };
int zero = 0;
int ptyno = 0;
int fd = open("/dev/ptmx", O_RDONLY);
int fd2;
pthread_t th;
buf[0x9] = 0xfd;
buf[0xc] = buf[0xd] = buf[0xe] = buf[0xf] = buf[0x10] = 0xff;
ioctl(fd, TCSETS, buf);
ioctl(fd, TIOCSPTLCK, &zero);
ioctl(fd, TCXONC, TCIOFF);
if (ioctl(fd, TIOCGPTN, &ptyno))
return -1;
sprintf(buf, "/dev/pts/%d", ptyno);
fd2 = open(buf, O_RDONLY);
pthread_create(&th, 0, thr, &fd2);
sleep(1);
ioctl(fd2, FIONREAD, buf);
return 0;
}
この不具合は以下のパッチにより修正されました。
これは、プロセスが終了できなくなるという不具合の再現プログラムです。
# https://syzkaller.appspot.com/bug?id=2ccac875e85dc852911a0b5b788ada82dc0a081e
# See https://goo.gl/kgGztJ for information about syzkaller reproducers.
#{"threaded":true,"collide":true,"repeat":true,"procs":1,"sandbox":"none","fault_call":-1,"tun":true,"netdev":true,"resetnet":true,"cgroups":true,"binfmt_misc":true,"close_fds":true,"tmpdir":true,"segv":true}
mkdirat(0xffffffffffffff9c, &(0x7f0000000000)='./file0\x00', 0x0)
pipe(0x0)
r0 = openat2$dir(0xffffffffffffff9c, &(0x7f0000000100)='./file0/file0\x00', &(0x7f0000000140)={0x4041}, 0x18)
pipe(&(0x7f0000000040)={<r1=>0xffffffffffffffff, <r2=>0xffffffffffffffff})
pipe(0x0)
r3 = openat2$dir(0xffffffffffffff9c, &(0x7f0000000100)='./file0/file0\x00', &(0x7f0000000140)={0x4041}, 0x18)
write$binfmt_script(r3, 0x0, 0x7ffffffff000)
write$nbd(r2, &(0x7f0000000180)=ANY=[@ANYRESOCT=r3], 0x1000)
splice(r1, 0x0, r0, 0x0, 0xffe0, 0x0)
↑の syzkaller 表記のプログラム(12行)に対して syz-prog2c -threaded -collide -repeat 0 -procs 1 -sandbox none -fault_call -1 -enable tun -enable netdev -enable resetnet -enable cgroups -enable binfmt_misc -enable close_fds -tmpdir -segv というコマンドを実行することでC言語に変換したものが↓のプログラム(584行)です。
// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <setjmp.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <linux/capability.h>
#include <linux/futex.h>
static __thread int skip_segv;
static __thread jmp_buf segv_env;
static void segv_handler(int sig, siginfo_t* info, void* ctx)
{
uintptr_t addr = (uintptr_t)info->si_addr;
const uintptr_t prog_start = 1 << 20;
const uintptr_t prog_end = 100 << 20;
if (__atomic_load_n(&skip_segv, __ATOMIC_RELAXED) &&
(addr < prog_start || addr > prog_end)) {
_longjmp(segv_env, 1);
}
exit(sig);
}
static void install_segv_handler(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
syscall(SYS_rt_sigaction, 0x20, &sa, NULL, 8);
syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8);
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = segv_handler;
sa.sa_flags = SA_NODEFER | SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
}
#define NONFAILING(...) \
{ \
__atomic_fetch_add(&skip_segv, 1, __ATOMIC_SEQ_CST); \
if (_setjmp(segv_env) == 0) { \
__VA_ARGS__; \
} \
__atomic_fetch_sub(&skip_segv, 1, __ATOMIC_SEQ_CST); \
}
static void sleep_ms(uint64_t ms)
{
usleep(ms * 1000);
}
static uint64_t current_time_ms(void)
{
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts))
exit(1);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
static void use_temporary_dir(void)
{
char tmpdir_template[] = "./syzkaller.XXXXXX";
char* tmpdir = mkdtemp(tmpdir_template);
if (!tmpdir)
exit(1);
if (chmod(tmpdir, 0777))
exit(1);
if (chdir(tmpdir))
exit(1);
}
static void thread_start(void* (*fn)(void*), void* arg)
{
pthread_t th;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 128 << 10);
int i;
for (i = 0; i < 100; i++) {
if (pthread_create(&th, &attr, fn, arg) == 0) {
pthread_attr_destroy(&attr);
return;
}
if (errno == EAGAIN) {
usleep(50);
continue;
}
break;
}
exit(1);
}
typedef struct {
int state;
} event_t;
static void event_init(event_t* ev)
{
ev->state = 0;
}
static void event_reset(event_t* ev)
{
ev->state = 0;
}
static void event_set(event_t* ev)
{
if (ev->state)
exit(1);
__atomic_store_n(&ev->state, 1, __ATOMIC_RELEASE);
syscall(SYS_futex, &ev->state, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1000000);
}
static void event_wait(event_t* ev)
{
while (!__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE))
syscall(SYS_futex, &ev->state, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, 0);
}
static int event_isset(event_t* ev)
{
return __atomic_load_n(&ev->state, __ATOMIC_ACQUIRE);
}
static int event_timedwait(event_t* ev, uint64_t timeout)
{
uint64_t start = current_time_ms();
uint64_t now = start;
for (;;) {
uint64_t remain = timeout - (now - start);
struct timespec ts;
ts.tv_sec = remain / 1000;
ts.tv_nsec = (remain % 1000) * 1000 * 1000;
syscall(SYS_futex, &ev->state, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, &ts);
if (__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE))
return 1;
now = current_time_ms();
if (now - start > timeout)
return 0;
}
}
static bool write_file(const char* file, const char* what, ...)
{
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);
int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return false;
if (write(fd, buf, len) != len) {
int err = errno;
close(fd);
errno = err;
return false;
}
close(fd);
return true;
}
#define MAX_FDS 30
static void setup_common()
{
if (mount(0, "/sys/fs/fuse/connections", "fusectl", 0, 0)) {
}
}
static void loop();
static void sandbox_common()
{
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
setpgrp();
setsid();
struct rlimit rlim;
rlim.rlim_cur = rlim.rlim_max = (200 << 20);
setrlimit(RLIMIT_AS, &rlim);
rlim.rlim_cur = rlim.rlim_max = 32 << 20;
setrlimit(RLIMIT_MEMLOCK, &rlim);
rlim.rlim_cur = rlim.rlim_max = 136 << 20;
setrlimit(RLIMIT_FSIZE, &rlim);
rlim.rlim_cur = rlim.rlim_max = 1 << 20;
setrlimit(RLIMIT_STACK, &rlim);
rlim.rlim_cur = rlim.rlim_max = 0;
setrlimit(RLIMIT_CORE, &rlim);
rlim.rlim_cur = rlim.rlim_max = 256;
setrlimit(RLIMIT_NOFILE, &rlim);
if (unshare(CLONE_NEWNS)) {
}
if (unshare(CLONE_NEWIPC)) {
}
if (unshare(0x02000000)) {
}
if (unshare(CLONE_NEWUTS)) {
}
if (unshare(CLONE_SYSVSEM)) {
}
typedef struct {
const char* name;
const char* value;
} sysctl_t;
static const sysctl_t sysctls[] = {
{"/proc/sys/kernel/shmmax", "16777216"},
{"/proc/sys/kernel/shmall", "536870912"},
{"/proc/sys/kernel/shmmni", "1024"},
{"/proc/sys/kernel/msgmax", "8192"},
{"/proc/sys/kernel/msgmni", "1024"},
{"/proc/sys/kernel/msgmnb", "1024"},
{"/proc/sys/kernel/sem", "1024 1048576 500 1024"},
};
unsigned i;
for (i = 0; i < sizeof(sysctls) / sizeof(sysctls[0]); i++)
write_file(sysctls[i].name, sysctls[i].value);
}
static int wait_for_loop(int pid)
{
if (pid < 0)
exit(1);
int status = 0;
while (waitpid(-1, &status, __WALL) != pid) {
}
return WEXITSTATUS(status);
}
static void drop_caps(void)
{
struct __user_cap_header_struct cap_hdr = {};
struct __user_cap_data_struct cap_data[2] = {};
cap_hdr.version = _LINUX_CAPABILITY_VERSION_3;
cap_hdr.pid = getpid();
if (syscall(SYS_capget, &cap_hdr, &cap_data))
exit(1);
const int drop = (1 << CAP_SYS_PTRACE) | (1 << CAP_SYS_NICE);
cap_data[0].effective &= ~drop;
cap_data[0].permitted &= ~drop;
cap_data[0].inheritable &= ~drop;
if (syscall(SYS_capset, &cap_hdr, &cap_data))
exit(1);
}
static int do_sandbox_none(void)
{
if (unshare(CLONE_NEWPID)) {
}
int pid = fork();
if (pid != 0)
return wait_for_loop(pid);
setup_common();
sandbox_common();
drop_caps();
if (unshare(CLONE_NEWNET)) {
}
loop();
exit(1);
}
#define FS_IOC_SETFLAGS _IOW('f', 2, long)
static void remove_dir(const char* dir)
{
DIR* dp;
struct dirent* ep;
int iter = 0;
retry:
dp = opendir(dir);
if (dp == NULL) {
if (errno == EMFILE) {
exit(1);
}
exit(1);
}
while ((ep = readdir(dp))) {
if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0)
continue;
char filename[FILENAME_MAX];
snprintf(filename, sizeof(filename), "%s/%s", dir, ep->d_name);
struct stat st;
if (lstat(filename, &st))
exit(1);
if (S_ISDIR(st.st_mode)) {
remove_dir(filename);
continue;
}
int i;
for (i = 0;; i++) {
if (unlink(filename) == 0)
break;
if (errno == EPERM) {
int fd = open(filename, O_RDONLY);
if (fd != -1) {
long flags = 0;
if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == 0) {
}
close(fd);
continue;
}
}
if (errno == EROFS) {
break;
}
if (errno != EBUSY || i > 100)
exit(1);
}
}
closedir(dp);
int i;
for (i = 0;; i++) {
if (rmdir(dir) == 0)
break;
if (i < 100) {
if (errno == EPERM) {
int fd = open(dir, O_RDONLY);
if (fd != -1) {
long flags = 0;
if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == 0) {
}
close(fd);
continue;
}
}
if (errno == EROFS) {
break;
}
if (errno == EBUSY) {
continue;
}
if (errno == ENOTEMPTY) {
if (iter < 100) {
iter++;
goto retry;
}
}
}
exit(1);
}
}
static void kill_and_wait(int pid, int* status)
{
kill(-pid, SIGKILL);
kill(pid, SIGKILL);
int i;
for (i = 0; i < 100; i++) {
if (waitpid(-1, status, WNOHANG | __WALL) == pid)
return;
usleep(1000);
}
DIR* dir = opendir("/sys/fs/fuse/connections");
if (dir) {
for (;;) {
struct dirent* ent = readdir(dir);
if (!ent)
break;
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
char abort[300];
snprintf(abort, sizeof(abort), "/sys/fs/fuse/connections/%s/abort",
ent->d_name);
int fd = open(abort, O_WRONLY);
if (fd == -1) {
continue;
}
if (write(fd, abort, 1) < 0) {
}
close(fd);
}
closedir(dir);
} else {
}
while (waitpid(-1, status, __WALL) != pid) {
}
}
static void setup_test()
{
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
setpgrp();
write_file("/proc/self/oom_score_adj", "1000");
}
static void close_fds()
{
int fd;
for (fd = 3; fd < MAX_FDS; fd++)
close(fd);
}
struct thread_t {
int created, call;
event_t ready, done;
};
static struct thread_t threads[16];
static void execute_call(int call);
static int running;
static void* thr(void* arg)
{
struct thread_t* th = (struct thread_t*)arg;
for (;;) {
event_wait(&th->ready);
event_reset(&th->ready);
execute_call(th->call);
__atomic_fetch_sub(&running, 1, __ATOMIC_RELAXED);
event_set(&th->done);
}
return 0;
}
static void execute_one(void)
{
int i, call, thread;
int collide = 0;
again:
for (call = 0; call < 9; call++) {
for (thread = 0; thread < (int)(sizeof(threads) / sizeof(threads[0]));
thread++) {
struct thread_t* th = &threads[thread];
if (!th->created) {
th->created = 1;
event_init(&th->ready);
event_init(&th->done);
event_set(&th->done);
thread_start(thr, th);
}
if (!event_isset(&th->done))
continue;
event_reset(&th->done);
th->call = call;
__atomic_fetch_add(&running, 1, __ATOMIC_RELAXED);
event_set(&th->ready);
if (collide && (call % 2) == 0)
break;
event_timedwait(&th->done, 45);
break;
}
}
for (i = 0; i < 100 && __atomic_load_n(&running, __ATOMIC_RELAXED); i++)
sleep_ms(1);
close_fds();
if (!collide) {
collide = 1;
goto again;
}
}
static void execute_one(void);
#define WAIT_FLAGS __WALL
static void loop(void)
{
int iter;
for (iter = 0;; iter++) {
char cwdbuf[32];
sprintf(cwdbuf, "./%d", iter);
if (mkdir(cwdbuf, 0777))
exit(1);
int pid = fork();
if (pid < 0)
exit(1);
if (pid == 0) {
if (chdir(cwdbuf))
exit(1);
setup_test();
execute_one();
exit(0);
}
int status = 0;
uint64_t start = current_time_ms();
for (;;) {
if (waitpid(-1, &status, WNOHANG | WAIT_FLAGS) == pid)
break;
sleep_ms(1);
if (current_time_ms() - start < 5 * 1000)
continue;
kill_and_wait(pid, &status);
break;
}
remove_dir(cwdbuf);
}
}
#ifndef __NR_openat2
#define __NR_openat2 437
#endif
uint64_t r[4] = {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff,
0xffffffffffffffff};
void execute_call(int call)
{
intptr_t res = 0;
switch (call) {
case 0:
NONFAILING(memcpy((void*)0x20000000, "./file0\000", 8));
syscall(__NR_mkdirat, 0xffffff9c, 0x20000000ul, 0ul);
break;
case 1:
syscall(__NR_pipe, 0ul);
break;
case 2:
NONFAILING(memcpy((void*)0x20000100, "./file0/file0\000", 14));
NONFAILING(*(uint64_t*)0x20000140 = 0x4041);
NONFAILING(*(uint64_t*)0x20000148 = 0);
NONFAILING(*(uint64_t*)0x20000150 = 0);
res = syscall(__NR_openat2, 0xffffffffffffff9cul, 0x20000100ul,
0x20000140ul, 0x18ul);
if (res != -1)
r[0] = res;
break;
case 3:
res = syscall(__NR_pipe, 0x20000040ul);
if (res != -1) {
NONFAILING(r[1] = *(uint32_t*)0x20000040);
NONFAILING(r[2] = *(uint32_t*)0x20000044);
}
break;
case 4:
syscall(__NR_pipe, 0ul);
break;
case 5:
NONFAILING(memcpy((void*)0x20000100, "./file0/file0\000", 14));
NONFAILING(*(uint64_t*)0x20000140 = 0x4041);
NONFAILING(*(uint64_t*)0x20000148 = 0);
NONFAILING(*(uint64_t*)0x20000150 = 0);
res = syscall(__NR_openat2, 0xffffffffffffff9cul, 0x20000100ul,
0x20000140ul, 0x18ul);
if (res != -1)
r[3] = res;
break;
case 6:
syscall(__NR_write, r[3], 0ul, 0x7ffffffff000ul);
break;
case 7:
NONFAILING(sprintf((char*)0x20000180, "%023llo", (long long)r[3]));
syscall(__NR_write, r[2], 0x20000180ul, 0x1000ul);
break;
case 8:
syscall(__NR_splice, r[1], 0ul, r[0], 0ul, 0xffe0ul, 0ul);
break;
}
}
int main(void)
{
syscall(__NR_mmap, 0x1ffff000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x20000000ul, 0x1000000ul, 7ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x21000000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
install_segv_handler();
use_temporary_dir();
do_sandbox_none();
return 0;
}
人力で解析を行って必要最小限の処理だけを抽出した再現プログラムです。
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
static char buffer[4096];
const int fd = open("/tmp/testfile", O_WRONLY | O_CREAT, 0600);
int pipe_fd[2] = { EOF, EOF };
pipe(pipe_fd);
write(pipe_fd[1], NULL, 4096);
write(pipe_fd[1], buffer, 4096);
splice(pipe_fd[0], NULL, fd, NULL, 65536, 0);
return 0;
}
この不具合は以下のパッチにより修正されました。
さらに修正すると、 syzbot では不具合とは判断されない種類の別の不具合の再現プログラムに化けました。
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
const int fd = open("/tmp/testfile", O_WRONLY | O_CREAT, 0600);
int pipe_fd[2] = { EOF, EOF };
pipe(pipe_fd);
write(pipe_fd[1], NULL, 4096);
splice(pipe_fd[0], NULL, fd, NULL, 65536, 0);
return 0;
}
この不具合は以下のパッチにより修正されました。
例2と例3で示した再現プログラムは、実行するのに root ユーザの権限を必要としていません。
つまり、シェルログインして任意のプログラムを実行できるユーザであれば引き起こせる Local DoS 攻撃が可能な脆弱性だったりします。
しかし、この程度の不具合はよくある話なので、いちいちCVE申請をしないようです。
CVE番号が割り当てられないということは、影響を受けるかどうかの確認もしていない可能性が高いということなので、あなたが使っているカーネルにも、 Local DoS 攻撃が可能な脆弱性が残ったままになっているかもしれませんね。
累計発生回数が55万回を超えている、参照カウントリークに関するこの不具合には、複数の不具合が集約されています。
修正が全部のツリーに反映されるまでクローズされないので、パッチが作成されてからクローズされるまでに数か月の時間が空きます。
その間に別の不具合が発見されて同じレポートに集約されてしまうという可能性が存在するので、新しい不具合が発見されていないかどうかを常に見張り続ける必要があります。
累計発生回数トップの座に君臨していた不具合である unregister_netdevice: waiting for DEV to become free (2) の場合、少なくとも5回の修正が行われました。
(1個目の修正が行われた時点では、まだ私はこの不具合に関与していなかったので、どのパッチにより修正されたのかを把握していません。)
この不具合はまだ終わっておらず、4回目の不具合報告が現在進行中のようです。
不具合を修正するパッチには、不具合を報告してくれた人へのクレジット(謝意)として、報告者の名前を含めることが期待されるのですが、パッチの作成者は、必ずしも報告者の名前を含めてくれるとは限りません。
その結果、 syzbot が発見した不具合が修正済みなのかどうかを追跡することが難しいという問題があります。
open キューに滞留してしまう原因の1つです。
unregister_netdevice: waiting for DEV to become free (2) の場合、グーグルさんのボットである syzbot が見つけた不具合なのに、何故かファーウェイさんのボットである Hulk Robot が見つけたことにされていました。
net-sysfs: Call dev_hold always in netdev_queue_add_kobject の時点では、 Hulk Robot も同じ不具合を見つけていたからなのかもしれないと思って黙っていました。
しかし、 その後に syzbot が発見して私が転送した不具合についても、 Hulk Robot が発見したことにされていたのです。
そのため、今度は「 Hulk Robot って何者ですか?」ってツッコミを入れました。
( Hulk Robot が差出人のメールを見たことが無いので、無条件に含めるような使われ方がされていないかどうか気になったのです。)
すると、何者なのかの説明の代わりに、静かに Reported-by: が修正されたパッチが投稿され、採用されました。
こんなところにも、グーグル対ファーウェイの競争が現れているのでしょうかねぇ?(笑)
何故か常に4GB付近のアドレスにアクセスしようとしてクラッシュしたりハングアップしたりするという不具合が報告されていました。
その原因は、入力値の検査不足により、0行または0桁という画面サイズを受け入れてしまったことにありました。
必要なバッファのサイズは行数と桁数に比例するので、0行または0桁が指定されることで0バイトのメモリ割り当て要求が行われていたのですが、( Linux カーネルの世界では0バイトのメモリ割り当て要求に対して ZERO_SIZE_PTR というアドレスを返却するため) NULL ではないということで処理が先に進んでしまうという不運とも組み合わさって、画面の最終行にカーソル位置を移動するために -1U << 1 という計算が行われた結果、4GB付近の特定アドレスへのアクセスが発生していたのでした。
また、画面の解像度を変更した場合に余白をクリアするための処理の中でハングアップするという不具合も報告されていました。
その原因は、ピクセルの解像度に基づいてテキストの行数/桁数を計算する際に、0行または0桁が指定された場合には現在の行数または桁数を維持するという例外的な動作が適用されてしまった結果、解像度に比例しない行数/桁数が使われてしまい、行数/桁数が解像度に比例していることを前提とした余白をクリアする処理において、整数アンダーフローに起因したメモリ破壊バグが発生していたのです。
そして、この不具合を退治したところ、たくさんの厄介系のレポートも同じ原因によるものだったことが判明したのでした。
メモリ破壊バグなら、どんな事象が発生しても不思議ではないというのを再認識した不具合でした。
パッチを採用してもらうためには関係者のレビュー/承認を得る必要があります。
せっかくパッチを作成できても、関係者が反応してくれないことが頻繁にあります。
↓
その結果、パッチの存在がそのまま忘れ去られて、死屍累々となります。
また、「不具合が修正されるペース」<「新しい不具合が見つかるペース」となるため、ダッシュボード( https://syzkaller.appspot.com/upstream )が長蛇の列になります。
反応が無い場合には ping 作戦を試します。
でも、 ping 作戦が通じない相手も少なくありません。
例えば splice() のリグレッションの場合、原因となったパッチの作者に対して、最初に巻き込んでから
プライベートメールも含めて3回 ping しても返事が来ませんでした。
こうなると、私のメールが届いているのかどうか疑いたくなりますね。
(実際、いくつかのメールサーバでは私が使っている i-love.sakura.ne.jp のIPアドレスがスパムメールの送信元として拒否リストに登録されていることにより、コンタクトできない状態が発生しています。)
最終的に、この不具合とは無関係な Linus さんを巻き込む ping 作戦を行ったことにより、ようやく修正されました。
作者からの返事を得られるようになるまでの待ち時間の長さという点でトップの座に居るのは
だと思います。
自分が使っていないモジュールでの不具合なので、どんな処理をしているのかも知らない状態からのスタートでした。
最初は、再現プログラムがあるのに再現しないという謎事象に悩まされました。
再現プログラムがあるのに再現しなかった原因が解決したので、手元の環境での調査ができるようになりました。
しかし、誰も修正しようとしないので、とりあえず部分的な修正を提案してみました。
すると、このモジュールの作者が登場して説明してくれたのですが、まだ根本原因は不明でした。
そして、デバッグ printk() を用いて試行錯誤した結果、根本原因が判明しました。
/dev/raw-gadget というインタフェース経由でユーザ空間からイベントを処理できるようにした際に、プロセスが終了した場合に自動的にファイルディスクリプタがクローズされるという処理に依存していたため、このモジュールのファイルディスクリプタをクローズする処理の中で /dev/raw-gadget のファイルディスクリプタがクローズされるのを永遠に待ってしまうというデッドロックが発生していたのです。
しかし、作者からの返事は3週間以上ありませんでした。
まぁ、 family emergency ということなら仕方がありませんが・・・。
しかし、そのまま再び応答が無くなってしまったので、今度は当該処理を丸ごと削除するという提案をしてみたのですが、却下されてしまいました。
今度は作者の側からパッチが提示されたのでコメントをしたのですが、また1か月近く無応答になってしまいました。
9月になってようやく迅速な応答が来るようになり、最終的なパッチが作成されました。
そして今月初めに、累計発生回数の3位に居座っていた不具合は、ようやく Fix Pending 行きになったのでした。
たったこれだけの修正をするのに、不具合が報告されてから1年以上かかってしまいました。
(2020年10月02日23時30分時点の画像です)
このようなデッドロックの可能性は、 lockdep を用いて検知することができないため、厄介です。
このようなデッドロック事例は他にも存在していて、例えば、ページフォールトというイベントをユーザ空間から処理しようとしたことに起因したデッドロック事例が報告されています。
カーネル内で処理していた内容を安易にユーザ空間に切り出してしまうと、デッドロックという形でしっぺ返しを喰らってしまうのです。
(2020年09月11日23時00分時点の画像です)
第1弾では、 printk() の拡張により corrupted report (2) の改善をした話を紹介しました。
open of /sys/kernel/debug/kcov failed (errno 2) というエラーによりテストが打ち切られてしまうという事象が多発していました。
不具合を見つける前にエラーによりテストが打ち切られるのはリソースの無駄遣いです。
そのため、このエラーの原因を解消したいと考えました。
デバッグ fprintf() パッチを適用したところ、 /sys/kernel/debug/ にマウントされている筈の debugfs だけでなく、 /sys/ にマウントされている筈の sysfs も消滅していることが判明しました。
umount -l / を実行すれば全てのマウントを解除できるのですが、 /proc/ にマウントされている proc は残っているという、予想外の結果が得られました。
そこで、 unshare() を呼び出すパッチを提案したところ、作者である Dmitry さんから、テストの開始前に unshare() を呼んでいるので、テストケースによるマウントの操作は影響しないようになっている筈という回答がありました。
しかし、私は TOMOYO Linux 向けのテストケースを作成していた時に、 systemd が使われている環境では systemd がマウント操作の伝搬方法を変更してしまうため、 unshare() を呼ぶだけではなく、マウント操作の伝搬方法を元に戻す必要があることを経験していました。
そのため、マウント操作の伝搬方法を元に戻すパッチを適用した結果、 /sys/kernel/debug/kcov が消滅するという謎の事象は無事に解消されました。
/sys/kernel/debug/kcov が消滅する事象が解決したのを受けて、現在は ENOSPC エラーで終了してしまう問題への対処を試みています。
テストケースの中で fallocate() など大型の書き込みを行っているので、テスト終了後にファイルを削除することで改善できるのではないかと予想しました。
しかし、既にテスト終了後にテストで使用したディレクトリごと削除するようになっているので影響しない筈という回答でした。
そのため、デバッグ fprintf() パッチを適用したところ、テストに使用するディレクトリの外部にファイルを作成されてしまっている状況が確認されました。
つまり、サンドボックスから抜け出されているということなのですが、その理由はまだ判明していません。
2020年10月上旬の時点では、 lockdep の容量制限によりテストが打ち切られてしまうという問題が上位を占めています。
不具合を見つける前に容量制限によりテストが打ち切られるのはリソースの無駄遣いです。
そのため、 lockdep の容量制限を緩和できるようにするパッチを作成して投稿しましたが、メンテナからの反応を得ることができませんでした。
そこで、 Andrew さんを巻き込んだ ping を打ってみたところ、メンテナである Peter さんからの反応があったのですが、「OK」ではなく「嫌!」という返事でした。
WARNING in print_bfs_bug については、パッチが提案され、解消されたようです。
残りの問題についての代替案や対応方針の返事を待っているという状況ですが、 Peter さんは無反応になってしまいました。
Linus さんも巻き込んだ ping を打ってみましたが、まだ反応はありません。
・・・とりあえず、2週間前に報告されて一気に累計発生回数トップに躍り出た(3~4秒に1回のペースでクラッシュしてしまうのでテストが捗らない)不具合を修正する方が優先かとは思いますが・・・どうも、 Peter さんのメールボックスが悲惨な状態になっている模様です。
(2020年10月26日08時00分時点の画像です)
古くから存在しているフレームバッファや VGA コンソールのコードには、不具合も多く残っています。
依存関係が複雑で修正することが難しく、修正したつもりが別の不具合を埋め込んでしまうこともあります。
例えば、
という不具合報告は、
という不具合に対して適用されたパッチ2個の内、前者の不具合が原因です。
簡単な不具合なら ping 作戦で強行突破してしまうこともあります。
正しく修正しようとすると大手術になってしまいそうなので回避策で済ませることもあります。
例えば、 Shift-PageUp/PageDown でスクロールを行う機能である fbcon_scrolldelta() での不具合は、メモリ解放タイミングを遅らせるという回避策で済ませました。
しかし、↑の不具合に含まれていた fbcon_scrolldelta() という処理の呼び出しは、およそ1か月後に↓の不具合を修正するパッチにより丸ごと削除されてしまいました。
この修正により、 KASAN: use-after-free Read in fbcon_cursor に対して適用された vt: defer kfree() of vc_screenbuf in vc_do_resize() の存在理由が消滅してしまいました。
このように、誰も使っていないと思われる機能が原因で発生した不具合に対しては、修正ではなく削除という判断がされることもあるのです。
その11日後、別の不具合についての再現プログラムが見つかったので、調査してみました。
その結果、実際よりも大きなフォントサイズを指定できることが原因と判明したのですが、その機能を利用しているプログラムを見つけることができなかったため、誰も使っていない可能性がある機能ということで削除するという提案を行い、受け入れられました。
この修正により、 KASAN: global-out-of-bounds Read in bit_putcs に対して適用されたパッチ2個の内、後者の変更内容は削除されることになりました。
ちなみに、この機能は25年前(フレームバッファが使用されるより前の時代)のプログラムのために追加されたようです。
こんな感じで、使われなくなった古いコードの不具合が潰されていくようです。
syzkaller が行っているファジングは非常に創造的で、思いがけない方法を用いてシステムをクラッシュさせます。
例えば、 Ctrl-Alt-Delete ボタンが押された場合の処理をプログラム的に呼び出してしまうことにより、正常なシャットダウンシーケンスを伴うシステムの再起動が発生するのですが、このような挙動も syzkaller にとってはクラッシュと判定する事象です。
[ 211.026157][ T2628] usb 3-1: config 1 has an invalid descriptor of length 0, skipping remainder of the config [ 211.036464][ T2628] usb 3-1: config 1 interface 0 altsetting 0 endpoint 0x1 has invalid wMaxPacketSize 0 [ 211.047100][ T2628] usb 3-1: config 1 interface 0 altsetting 0 bulk endpoint 0x1 has invalid maxpacket 0 [ 211.056831][ T2628] usb 3-1: config 1 interface 0 altsetting 0 has 1 endpoint descriptor, different from the interface descriptor's value: 18 [[0;32m OK [0m] Unmounted /syzcgroup/unified. [[0;32m OK [0m] Reached target Unmount All Filesystems. [[0;32m OK [0m] Stopped target Local File Systems (Pre). [ 211.166150][ T2628] usb 3-1: New USB device found, idVendor=0525, idProduct=a4a8, bcdDevice= 0.07 [ 211.175550][ T2628] usb 3-1: New USB device strings: Mfr=0, Product=0, SerialNumber=1 [ 211.183608][ T2628] usb 3-1: SerialNumber: syz [[0;32m OK [0[ 211.209083][ T7168] usb 4-1: New USB device found, idVendor=08ca, idProduct=0021, bcdDevice=da.16 m[ 211.219914][ T7168] usb 4-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 ] [ 211.227936][ T7168] usb 4-1: Product: syz [ 211.232277][ T7168] usb 4-1: Manufacturer: syz [ 211.237021][ T7168] usb 4-1: SerialNumber: syz [ 211.242156][T14522] udc-core: couldn't find an available UDC or it's busy [ 211.249293][T14522] misc raw-gadget: fail, usb_gadget_probe_driver returned -16 Stopped Create Static Device Nodes in /dev. [ 211.283282][ T7168] usb 4-1: config 0 descriptor?? [ 211.296000][ T2628] hub 3-1:1.0: bad descriptor, ignoring hub [ 211.302017][ T2628] hub: probe of 3-1:1.0 failed with error -5 [[0;32m OK [0m] Stopped Remount Root and Kernel File Systems. [[0;32m OK [0m] Reached target Shutdown. [ 211.574796][ T6775] usb 6-1: new high-speed USB device number 90 using dummy_hcd [ 211.592163][ T1] printk: systemd-shutdow: 43 output lines suppressed due to ratelimiting [ 211.651002][ T1] systemd-shutdown[1]: Sending SIGTERM to remaining processes... [ 211.864658][ T6775] usb 6-1: Using ep0 maxpacket: 16 [ 211.896129][ T2628] usblp 3-1:1.0: usblp0: USB Unidirectional printer dev 90 if 0 alt 0 proto 1 vid 0x0525 pid 0xA4A8 [ 211.954447][ T12] asix 2-1:0.0 (unnamed net_device) (uninitialized): Failed to write reg index 0x0012: -71 [ 211.974897][ T2628] usb 3-1: USB disconnect, device number 90 [ 211.984907][ T7168] aiptek 4-1:0.0: Aiptek using 400 ms programming speed [ 212.004710][ T7168] input: Aiptek as /devices/platform/dummy_hcd.3/usb4/4-1/4-1:0.0/input/input34 [ 212.014704][ T6775] usb 6-1: device descriptor read/all, error -71 [ 212.015237][ T12] asix: probe of 2-1:0.0 failed with error -71 [ 212.028591][ T2628] usblp0: removed [ 212.068638][ T12] usb 2-1: USB disconnect, device number 89 [ 212.117765][ T7168] input: failed to attach handler kbd to device input34, error: -5 [ 212.137563][ T7168] usb 4-1: USB disconnect, device number 96 [ 212.153134][ T2616] systemd-journald[2616]: Received SIGTERM from PID 1 (systemd-shutdow). [ 212.312653][ T1] systemd-shutdown[1]: Sending SIGKILL to remaining processes... [ 212.387547][ T1] systemd-shutdown[1]: Unmounting file systems. [ 212.396261][ T1] systemd-shutdown[1]: Remounting '/' read-only with options ''. [ 212.437151][ T1] EXT4-fs (sda1): re-mounted. Opts: [ 212.462501][ T1] systemd-shutdown[1]: Remounting '/' read-only with options ''. [ 212.478424][ T1] EXT4-fs (sda1): re-mounted. Opts: [ 212.484014][ T1] systemd-shutdown[1]: All filesystems unmounted. [ 212.490469][ T1] systemd-shutdown[1]: Deactivating swaps. [ 212.496992][ T1] systemd-shutdown[1]: All swaps deactivated. [ 212.503304][ T1] systemd-shutdown[1]: Detaching loop devices. [ 212.634808][ T1] systemd-shutdown[1]: All loop devices detached. [ 213.521977][ T17] rtl8150 5-1:0.0: couldn't reset the device [ 213.528115][ T17] rtl8150: probe of 5-1:0.0 failed with error -5 [ 213.537501][ T17] usb 5-1: USB disconnect, device number 77 [ 215.909342][ T7351] rtl8187: Invalid hwaddr! Using randomly generated MAC address [ 234.254771][ T7351] ieee80211 phy37: hwaddr de:dd:33:09:8f:4e, RTL8187vB (default) V1 + rtl8225, rfkill mask 2 [ 235.980120][ T7351] rtl8187: Customer ID is 0x00 [ 235.985827][ T7351] leds rtl8187-phy37::radio: led_trigger_set: Error sending uevent [ 235.994651][ T7351] leds rtl8187-phy37::tx: led_trigger_set: Error sending uevent [ 236.002733][ T7351] leds rtl8187-phy37::rx: led_trigger_set: Error sending uevent [ 236.069486][ T7351] rtl8187: wireless switch is off [ 236.076571][ T7351] usb 1-1: USB disconnect, device number 86 [ 236.083644][ T7351] leds rtl8187-phy37::radio: led_trigger_set: Error sending uevent [ 236.092112][ T7351] leds rtl8187-phy37::rx: led_trigger_set: Error sending uevent [ 236.100679][ T7351] leds rtl8187-phy37::tx: led_trigger_set: Error sending uevent [ 236.350885][ T1] sd 0:0:1:0: [sda] Synchronizing SCSI cache [ 236.358554][ T1] reboot: Restarting system [ 236.363168][ T1] reboot: machine restart SeaBIOS (version 1.8.2-google) Total RAM Size = 0x0000000200000000 = 8192 MiB CPUs found: 2 Max CPUs supported: 2 found virtio-scsi at 0:3 virtio-scsi vendor='Google' product='PersistentDisk' rev='1' type=0 removable=0 virtio-scsi blksize=512 sectors=4194304 = 2048 MiB drive 0x000f22f0: PCHS=0/0/0 translation=lba LCHS=520/128/63 s=4194304 Sending Seabios boot VM event. Booting from Hard Disk 0... early console in extract_kernel input_data: 0x00000000080392e0 input_len: 0x00000000027c6f0a output: 0x0000000001000000 output_len: 0x0000000008579a40 kernel_total_size: 0x0000000009826000 needed_size: 0x0000000009a00000 trampoline_32bit: 0x000000000009d000 Decompressing Linux... Parsing ELF... done. Booting the kernel. [ 0.000000][ T0] Linux version 5.9.0-syzkaller (syzkaller@syzkaller) (gcc (GCC) 10.1.0-syz 20200507, GNU ld (GNU Binutils for Ubuntu) 2.26.1) #0 SMP now [ 0.000000][ T0] Command line: BOOT_IMAGE=/vmlinuz root=/dev/sda1 console=ttyS0 earlyprintk=serial vsyscall=native oops=panic panic_on_warn=1 nmi_watchdog=panic panic=86400 net.ifnames=0 sysctl.kernel.hung_task_all_cpu_backtrace=1 ima_policy=tcb workqueue.watchdog_thresh=140 kvm-intel.nested=1 nf-conntrack-ftp.ports=20000 nf-conntrack-tftp.ports=20000 nf-conntrack-sip.ports=20000 nf-conntrack-irc.ports=20000 nf-conntrack-sane.ports=20000 vivid.n_devs=16 vivid.multiplanar=1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2 netrom.nr_ndevs=16 rose.rose_ndevs=16 spec_store_bypass_disable=prctl numa=fake=2 nopcid dummy_hcd.num=8 binder.debug_mask=0 rcupdate.rcu_expedited=1 [ 0.000000][ T0] KERNEL supported cpus: [ 0.000000][ T0] Intel GenuineIntel [ 0.000000][ T0] AMD AuthenticAMD [ 0.000000][ T0] x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers' [ 0.000000][ T0] x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers' [ 0.000000][ T0] x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers' [ 0.000000][ T0] x86/fpu: xstate_offset[2]: 576, xstate_sizes[2]: 256 [ 0.000000][ T0] x86/fpu: Enabled xstate features 0x7, context size is 832 bytes, using 'standard' format. [ 0.000000][ T0] BIOS-provided physical RAM map: [ 0.000000][ T0] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable [ 0.000000][ T0] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved [ 0.000000][ T0] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved [ 0.000000][ T0] BIOS-e820: [mem 0x0000000000100000-0x00000000bfffcfff] usable [ 0.000000][ T0] BIOS-e820: [mem 0x00000000bfffd000-0x00000000bfffffff] reserved [ 0.000000][ T0] BIOS-e820: [mem 0x00000000fffbc000-0x00000000ffffffff] reserved [ 0.000000][ T0] BIOS-e820: [mem 0x0000000100000000-0x000000023fffffff] usable [ 0.000000][ T0] printk: bootconsole [earlyser0] enabled [ 0.000000][ T0] Malformed early option 'vsyscall' [ 0.000000][ T0] Malformed early option 'numa' [ 0.000000][ T0] nopcid: PCID feature disabled [ 0.000000][ T0] NX (Execute Disable) protection: active [ 0.000000][ T0] SMBIOS 2.4 present. [ 0.000000][ T0] DMI: Google Google Compute Engine/Google Compute Engine, BIOS Google 01/01/2011 [ 0.000000][ T0] Hypervisor detected: KVM [ 0.000000][ T0] kvm-clock: Using msrs 4b564d01 and 4b564d00 [ 0.000000][ T0] kvm-clock: cpu 0, msr 919f001, primary cpu clock [ 0.000004][ T0] kvm-clock: using sched offset of 3087683385 cycles [ 0.000919][ T0] clocksource: kvm-clock: mask: 0xffffffffffffffff max_cycles: 0x1cd42e4dffb, max_idle_ns: 881590591483 ns [ 0.003833][ T0] tsc: Detected 2300.000 MHz processor [ 0.005875][ T0] last_pfn = 0x240000 max_arch_pfn = 0x400000000 [ 0.006961][ T0] x86/PAT: Configuration [0-7]: WB WC UC- UC WB WP UC- WT [ 0.007999][ T0] last_pfn = 0xbfffd max_arch_pfn = 0x400000000 [ 0.014265][ T0] found SMP MP-table at [mem 0x000f25b0-0x000f25bf] [ 0.015322][ T0] Using GB pages for direct mapping
不具合を検出するツールの立場としては、できる限り広い範囲をテストしたいのですが、 syzkaller はサンドボックスから脱出する方法を発見してしまうくらいに予想外の挙動をしてしまいます。
例えば、 /dev/mem へのアクセスを拒否リストで禁止しているにも関わらず、マウント操作と組合せることにより、 /dev/mem というパス名を使わずに /dev/mem にアクセスされてしまうということもありました。
よって、管理者権限での操作をピンポイントで阻止するためには、カーネル側で阻止する必要があります。
それを可能にするには、ファジングテスト用カーネルで使用するためのカーネルコンフィグオプションが必要になると考えています。
そこで、ファジングテスト用カーネルで使用するためのカーネルコンフィグオプションを Linux カーネル本体に追加しようとしました。
その際、 Linux 本体に追加できるようにするために、 syzbot や syzkaller という名前を使わない形を提案しました。
すると、 Linus さんが「個別にロックダウンできるようにしてほしい」とコメントしてきました。
そこで、個別にロックダウンできるようにするために、カーネルコンフィグオプションを1個ではなく複数個に分割する形を提案しました。
しかし、 Linus さんが「カーネルコンフィグオプションで切り替えるのは嫌だ」とコメントしてきました。
カーネルコンフィグオプションでコンパイル時に切り替えないということは、フラグとなる変数がカーネルメモリ内に存在することになります。
そのため、ランダムメモリ破壊バグによって意図せずにフラグが書き換わってしまう可能性があります。
また、( /dev/mem にアクセスする抜け道を見つけてしまうのと同様に)起動後に /sys/kernel/debug/ などのインタフェース経由で合法的にフラグを書き換えてしまう可能性もあります。
従って、起動後に切り替えできるようにするという選択肢は困難です。
どんなに譲歩しても、起動時に切り替えできるようにするというところまでだと予想されます。
Linus さんのコメントに関してはそれ以上の進展はありませんでした。
Linus さん以外との議論では、個別に切り替えできるようにするために、複数個のカーネルコンフィグオプションを用いる方向で話が進んでいきました。
そして、個別の切り替えができるようにするためのカーネルコンフィグオプションである CONFIG_TWIST_KERNEL_BEHAVIOR と、 syzkaller で使いたいカーネルコンフィグオプションを纏めて選択するためのカーネルコンフィグオプションである CONFIG_TWIST_FOR_SYZKALLER_TESTING を使うという形で話はいったん落ち着きました。
そして、 4/24 に Andrew さんにパッチを拾われて、 4/28 から linux-next でのテスト期間に入りました。
Linux 5.8 のマージウィンドウで採用されることを期待していたのですが、藪蛇の可能性がある状況が発生してしまいました。
printk() の一種である pr_debug() という関数は、デバッグレベルでメッセージを出力するための関数なので、 DEBUG が定義されていない場合にはメッセージを出力しないというマクロとして定義されています。
そのため、もし pr_debug() に渡された引数を評価していればキャッチできたであろう NULL pointer dereference 不具合を見落として、関係ない SELinux の不具合として報告されるということがありました。
そこで、ファジングテスト向けのカーネルでは常に pr_debug() に渡された引数を評価できるようにするために、 CONFIG_TWIST_ALWAYS_EVALUATE_DEBUG_PRINTK というカーネルコンフィグオプションを追加するという提案を行いました。
すると、 Linux 5.8 のマージウィンドウがオープンされる直前になって Linus さんが再度「カーネルコンフィグオプションはダメだ」と反対したのです。
長いコマンドラインオプションを指定できるようにするために initramfs の中にカーネルコマンドラインオプションを埋め込む boot-config ファイルという機能が Linux 5.6 で追加されたので、それを使えという主張です。
追加しようとしているのは、処理の無効化や情報取得支援のためのコードおよびフラグです。
これらはデバッグ支援に分類される機能であるため、大部分の人は必要としないものです。
CONFIG_TWIST_ALWAYS_EVALUATE_DEBUG_PRINTK のようなカーネルコンフィグオプションで除外できない場合、全員に対して不要な処理/データを保持することを強制することになるため、メモリ消費/処理速度/プログラムのサイズという点で影響が生じます。
また、殆どの利用者には興味のない大量のカーネルコマンドラインオプションを定義する必要があります。
syzkaller では、必要な機能を全てビルトインにした状態で vmlinux を作成するため、システムを起動するために initramfs を使う必要がありません。
しかし、カーネルコマンドラインオプションで切り替えできるようにするためには boot-config ファイルを含んだ initramfs の利用を強制する必要があるということです。
Google 関係者がインターンシップ対応で9月末まで忙しかったので、一時停止の状態が続いていました。
Linux 5.10 のマージウィンドウも昨日クローズされたので、この講演が終わったら再開したいと思っていますが、他のカーネル開発者からの協力が無いと突破するのは困難そうです。
syzkaller では、カーネルのコマンドラインが不具合報告の中に含まれていなかったことで事象を再現できなかったという事例に既に遭遇しており、起動時のカーネルコマンドラインオプションで行っていた「どのLSMを有効にするか」という指定をコンパイル時のカーネルコンフィグオプションで行うように変更するという修正が行われました。
管理者権限での操作を期待した通りにピンポイントで阻止するために initramfs を更新してもらうというミスを招きやすい操作を、カーネルのデバッグに関わる人や興味のある人に強制できるものなのでしょうか?
果たして、カーネルコンフィグオプションという選択肢で Linus さんを説得できるでしょうか?
他にもいろいろありますが、たぶんもう時間切れになっている筈なので割愛します。
今回は「 syzbot に追い回される開発者たち?」というタイトルでしたが、熊猫が syzbot を追いかけることを通じて、熊猫がカーネル開発者たちを追い回しているのではないかという感じがしないでもありません。(笑)
熊猫は2011年から2019年まで、セキュリティ・キャンプで講演をしてきました。
でも、熊猫が訴えてきたのは、「技術によるセキュリティ」ではなく「考えることによるセーフティ」であると思います。
自然災害が多発している昨今、人の意思で予防できる人災に対して、今後も技術に頼ったセキュリティを続けていくのでしょうか?