/* * Test program for Linux poison memory error recovery. * This program is extended from tinjpage with a multi-process model. * * This injects poison into various mapping cases and triggers the poison * handling. Requires special injection support in the kernel. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; version * 2. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should find a copy of v2 of the GNU General Public License somewhere * on your Linux system; if not, write to the Free Software Foundation, * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: Andi Kleen, Fengguang Wu, Haicheng Li * */ #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MADV_POISON 100 #define PAGE_SIZE 4 * 1024 #define SHM_SIZE 1 // in page_size. #define SHM_MODE 0600 #define FILE_SIZE 1 * 1024 * 1024 * 1024 #define LOG_BUFLEN 100 #define INSTANCE_NUM 10000 #define TEST_PASS 1 #define TEST_FAIL 0 static int PS = PAGE_SIZE; static int instance = 0; // index of the child process. static int testid = 0; // test index of the child process. static int test_types = 0; // totoal test types. static int t_shm = -1; // index of shm test case. static int failure = 0; // result of child process. static int unexpected = 0; // result of child process. static int early_kill = 0; struct test { int id; int result; }; struct shm { int id; int ready; int done; }; struct ipc { struct test test[INSTANCE_NUM]; struct shm shm; }; static int ipc_entry; static int *shmptr = NULL; static pid_t g_pid[INSTANCE_NUM] = { 0 }; static int shm_size = SHM_SIZE; static int child_num = INSTANCE_NUM; static int shm_child_num = 0; static char log_file[PATH_MAX]; static FILE *log_fd = NULL; static char result_file[PATH_MAX]; static FILE *result_fd = NULL; static char tmp_dir[PATH_MAX] = { '\0' }; static int clean_env = 0; static int semid_ready = 0; static pid_t mypid; union semun { int val; struct semid_ds *buf; unsigned short int *array; struct semid_info *__buf; }; enum rmode { MREAD = 0, MWRITE = 1, MREAD_OK = 2, MWRITE_OK = 3, MNOTHING = -1, }; static struct option opts[] = { {"clean", 0, 0, 'C'}, {"help", 0, 0, 'h'}, {"instance", 0, 0, 'i'}, {"log", 0, 0, 'l'}, {"result", 0, 0, 'r'}, {"shmsize", 0, 0, 's'}, {"tmpdir", 0, 0, 't'}, {"", 0, 0, '\0'} }; static void help(void) { printf("Usage: page-poisoning [OPTION]...\n" "Stress test for Linux HWPOISON Page Recovery with multiple processes.\n" "\n" "Mandatory arguments to long options are mandatory for short options too.\n" " -C, --clean record log and result in clean files.\n" " -h print this page\n" " -i, --child_num=NUM spawn NUM processes to do test (default NUM = %d)\n" " -l, --log=LOG record logs to file LOG.\n" " -r, --result=RESULT record test result to file RESULT.\n" " -s, --shmsize=SIZE each shared memory segment is SIZE-page based.\n" " -t, --tmpdir=DIR create temporary files in DIR.\n\n", INSTANCE_NUM); } static void err(const char *fmt, ...); static void mylog(const char *fmt, ...) { char buf[LOG_BUFLEN] = { '\0' }; va_list args; if (!log_fd) err("no log file there\n"); va_start(args, fmt); vsprintf(buf, fmt, args); printf("[pid %d] %s", mypid, buf); fprintf(log_fd, "[pid %d] %s", mypid, buf); fflush(log_fd); va_end(args); } static void result(const char *fmt, ...) { char buf[LOG_BUFLEN] = { '\0' }; va_list args; if (!result_fd) err("no result file there\n"); va_start(args, fmt); vsprintf(buf, fmt, args); fprintf(result_fd, "[pid %d] %s", mypid, buf); fflush(result_fd); if (log_fd) mylog("%s", buf); va_end(args); } static void err(const char *fmt, ...) { char buf[LOG_BUFLEN] = { '\0' }; va_list args; va_start(args, fmt); vsprintf(buf, fmt, args); if (result_fd) result("error: %s :%s\n", buf, strerror(errno)); else perror(buf); va_end(args); exit(1); } static void *checked_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset) { void *map = mmap(start, length, prot, flags, fd, offset); if (map == (void *)-1L) err("mmap"); return map; } static void munmap_reserve(void *page, int size) { munmap(page, size); mmap(page, size, PROT_NONE, MAP_PRIVATE | MAP_FIXED, 0, 0); } static void *xmalloc(size_t s) { void *p = malloc(s); if (!p) exit(ENOMEM); return p; } static int recovercount; static sigjmp_buf recover_ctx; static sigjmp_buf early_recover_ctx; static void *expected_addr; static void sighandler(int sig, siginfo_t * si, void *arg) { mylog("signal %d code %d addr %p\n", sig, si->si_code, si->si_addr); if (si->si_addr != expected_addr) { result("failed: Unexpected address in signal %p (expected %p)\n", si->si_addr, expected_addr); failure++; } if (--recovercount == 0) { result("failed: I seem to be in a signal loop. bailing out.\n"); exit(1); } if (si->si_code == 4) siglongjmp(recover_ctx, 1); else siglongjmp(early_recover_ctx, 1); } static void poison(char *msg, char *page, enum rmode mode) { expected_addr = page; recovercount = 5; if (sigsetjmp(early_recover_ctx, 1) == 0) { if (madvise(page, PS, MADV_POISON) != 0) { if (errno == EINVAL) { result("failed: Kernel doesn't support poison injection\n"); exit(0); } err("error: madvise: %s", strerror(errno)); return; } if (early_kill && (mode == MWRITE || mode == MREAD)) { result("failed: %s: process is not early killed\n", msg); failure++; } return; } if (early_kill) { if (mode == MREAD_OK || mode == MWRITE_OK) { result("failed: %s: killed\n", msg); failure++; } else mylog("pass: recovered\n"); } } static void recover(char *msg, char *page, enum rmode mode) { expected_addr = page; recovercount = 5; if (sigsetjmp(recover_ctx, 1) == 0) { switch (mode) { case MWRITE: mylog("writing 2\n"); *page = 2; break; case MWRITE_OK: mylog("writing 4\n"); *page = 4; return; case MREAD: mylog("reading %x\n", *(unsigned char *)page); break; case MREAD_OK: mylog("reading %x\n", *(unsigned char *)page); return; case MNOTHING: return; } /* signal or kill should have happened */ result("failed: %s: page is not poisoned after injection\n", msg); failure++; return; } if (mode == MREAD_OK || mode == MWRITE_OK) { result("failed: %s: killed\n", msg); failure++; } else mylog("pass: recovered\n"); } static void testmem(char *msg, char *page, enum rmode mode) { mylog("%s poisoning page %p\n", msg, page); poison(msg, page, mode); recover(msg, page, mode); } static void expecterr(char *msg, int err) { if (err) { mylog("pass: expected error %d on %s\n", errno, msg); } else { result("failed: unexpected no error on %s\n", msg); failure++; } } /* * Any optional error is really a deficiency in the kernel VFS error reporting * and should be eventually fixed and turned into a expecterr */ static void optionalerr(char *msg, int err) { if (err) { mylog("pass: expected error %d on %s\n", errno, msg); } else { mylog("LATER: expected likely incorrect no error on %s\n", msg); unexpected++; } } static int playfile(char *buf) { int fd; if (buf[0] == 0) snprintf(buf, PATH_MAX, "%s/dirty%d", tmp_dir, mypid); fd = open(buf, O_CREAT | O_RDWR | O_TRUNC, 0600); if (fd < 0) err("opening temporary file: %s", buf); const int NPAGES = 5; char *tmp = xmalloc(PS * NPAGES); int i; for (i = 0; i < PS * NPAGES; i++) tmp[i] = i; write(fd, tmp, PS * NPAGES); lseek(fd, 0, SEEK_SET); free(tmp); return fd; } static void dirty_anonymous(void) { struct ipc *ipc; char *page; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; page = checked_mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, 0, 0); testmem("dirty", page, MWRITE); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static void dirty_anonymous_unmap(void) { struct ipc *ipc; char *page; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; page = checked_mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, 0, 0); testmem("dirty", page, MWRITE); munmap_reserve(page, PS); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static void mlocked_anonymous(void) { struct ipc *ipc; char *page; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; page = checked_mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_LOCKED, 0, 0); testmem("mlocked", page, MWRITE); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static void do_file_clean(int flags, char *name) { char *page; char fn[PATH_MAX]; snprintf(fn, PATH_MAX, "%s/clean%d", tmp_dir, mypid); int fd = open(fn, O_RDWR | O_TRUNC | O_CREAT, 0600); if (fd < 0) err("opening temporary file: %s", fn); write(fd, fn, 4); page = checked_mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_SHARED | flags, fd, 0); fsync(fd); close(fd); testmem(name, page, MREAD_OK); /* reread page from disk */ mylog("reading %x\n", *(unsigned char *)page); testmem(name, page, MWRITE_OK); } static void file_clean(void) { struct ipc *ipc; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; do_file_clean(0, "file clean"); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static void file_clean_mlocked(void) { struct ipc *ipc; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; do_file_clean(MAP_LOCKED, "file clean mlocked"); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static char *ndesc(char *buf, char *name, char *add) { snprintf(buf, 100, "%s %s", name, add); return buf; } static void do_file_dirty(int flags, char *name) { char nbuf[100]; char *page; char fn[PATH_MAX]; fn[0] = 0; int fd = playfile(fn); page = checked_mmap(NULL, PS, PROT_READ, MAP_SHARED | MAP_POPULATE | flags, fd, 0); testmem(ndesc(nbuf, name, "initial"), page, MREAD); expecterr("msync expect error", msync(page, PS, MS_SYNC) < 0); close(fd); munmap_reserve(page, PS); fd = open(fn, O_RDONLY); if (fd < 0) err("reopening temp file"); page = checked_mmap(NULL, PS, PROT_READ, MAP_SHARED | MAP_POPULATE | flags, fd, 0); recover(ndesc(nbuf, name, "populated"), page, MREAD_OK); close(fd); munmap_reserve(page, PS); fd = open(fn, O_RDONLY); if (fd < 0) err("reopening temp file"); page = checked_mmap(NULL, PS, PROT_READ, MAP_SHARED | flags, fd, 0); recover(ndesc(nbuf, name, "fault"), page, MREAD_OK); close(fd); munmap_reserve(page, PS); fd = open(fn, O_RDWR); char buf[128]; /* the earlier close has eaten the error */ optionalerr("explicit read after poison", read(fd, buf, sizeof buf) < 0); optionalerr("explicit write after poison", write(fd, "foobar", 6) < 0); optionalerr("fsync expect error", fsync(fd) < 0); close(fd); /* should unlink return an error here? */ if (unlink(fn) < 0) perror("unlink"); } static void file_dirty(void) { struct ipc *ipc; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; do_file_dirty(0, "file dirty"); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static void file_dirty_mlocked(void) { struct ipc *ipc; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; do_file_dirty(MAP_LOCKED, "file dirty mlocked"); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } static void request_sem(int id, int num) { struct sembuf sb; sb.sem_num = num; sb.sem_op = -1; sb.sem_flg = 0; semop(id, &sb, 1); } static void waiton_sem(int id, int num) { struct sembuf sb; sb.sem_num = num; sb.sem_flg = 0; sb.sem_op = -1; semop(id, &sb, 1); sb.sem_op = 0; semop(id, &sb, 1); } static void release_sem(int id, int num) { struct sembuf sb; sb.sem_num = num; sb.sem_op = 1; sb.sem_flg = 0; semop(id, &sb, 1); } static void clean_anonymous(void) { char *page; page = checked_mmap(NULL, PS, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); testmem("clean", page, MWRITE_OK); } static void anon_clean(void) { struct ipc *ipc; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; clean_anonymous(); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } /* TBD static void survival(void) { struct ipc *ipc; char page; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; testmem("survial", &page, MNOTHING); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(ipc); } */ static void shm_test(void) { struct ipc *ipc; if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); ipc->test[instance].id = testid; request_sem(semid_ready, 0); if (!ipc->shm.ready) { if ((ipc->shm.id = shmget(IPC_PRIVATE, shm_size * PS, SHM_MODE)) < 0) err("shmget error\n"); ipc->shm.ready = 1; } if ((shmptr = shmat(ipc->shm.id, 0, 0)) == (void *)-1) { err("shmat error\n"); } else *shmptr = mypid; release_sem(semid_ready, 0); waiton_sem(semid_ready, 1); request_sem(semid_ready, 0); if (!ipc->shm.done) { ipc->shm.done = 1; testmem("shm dirty", (char *)shmptr, MWRITE); } else recover("shm dirty", (char *)shmptr, MREAD); release_sem(semid_ready, 0); if (!failure) ipc->test[instance].result = TEST_PASS; shmdt(shmptr); shmdt(ipc); } static void setup_ipc(void) { int size; union semun sunion; struct ipc *ipc; size = sizeof(struct ipc); if ((ipc_entry = shmget(IPC_PRIVATE, size, SHM_MODE)) < 0) err("shmget error\n"); if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); memset(ipc, 0, sizeof(struct ipc)); ipc->shm.id = -1; shmdt(ipc); semid_ready = semget(IPC_PRIVATE, 2, SHM_R | SHM_W); sunion.val = 1; semctl(semid_ready, 0, SETVAL, sunion); if (t_shm != -1) { if (((child_num - 1) % test_types) >= t_shm) shm_child_num = (child_num - 1) / test_types + 1; else shm_child_num = (child_num - 1) / test_types; } if (shm_child_num) { sunion.val = shm_child_num; semctl(semid_ready, 1, SETVAL, sunion); mylog("there are %d shm_child\n", shm_child_num); } } static void free_ipc(void) { struct ipc *ipc; semctl(semid_ready, 0, IPC_RMID); if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); if (ipc->shm.id != -1) shmctl(ipc->shm.id, IPC_RMID, 0); shmdt(ipc); shmctl(ipc_entry, IPC_RMID, 0); } static void cleanup(void) { int i; for (i = 0; i < instance; i++) kill(g_pid[i], 9); //kill the suviving child. free_ipc(); } struct testcase { void (*f) (void); char *name; int survivable; } cases[] = { { shm_test, "shared memory test", 0}, { anon_clean, "anonymous clean", 1}, { dirty_anonymous, "anonymous dirty", 0}, { dirty_anonymous_unmap, "anonymous dirty unmap", 0}, { mlocked_anonymous, "anonymous dirty mlocked", 0}, { file_clean, "file clean", 1}, { file_dirty, "file dirty", 0}, { file_clean_mlocked, "file clean mlocked", 1}, { file_dirty_mlocked, "file dirty mlocked", 0}, // { survival, "survival", 0 }, { NULL, NULL, 0} }; static int run_test(int children) { pid_t pid = -1; int i = 0, rc = 0; siginfo_t sig; struct ipc *ipc; for (i = 0; i < children; i++) { pid = fork(); if (pid < 0) { err("fork %d\n", i); break; } else if (pid == 0) { int j = instance % test_types; mypid = getpid(); testid = j; cases[j].f(); exit(0); } else { g_pid[i] = pid; ++instance; fflush(stdout); } } mylog("have spawned %d processes\n", instance); if (instance) { if ((ipc = shmat(ipc_entry, 0, 0)) == (void *)-1) err("shmat error\n"); for (i = 0; i < instance; i++) { int t = ipc->test[i].id; mylog("wait for Pid %d\n", g_pid[i]); waitid(P_PID, g_pid[i], &sig, WEXITED); if (ipc->test[i].result == TEST_PASS) result("Ins %d: Pid %d: pass - %s\n", i, g_pid[i], cases[t].name); else { result("Ins %d: Pid %d: failed - %s\n", i, g_pid[i], cases[t].name); failure++; } } shmdt(ipc); } if (!failure) result("\t!!! Page Poisoning Test got PASS. !!!\n\n"); else { result("\t!!! Page Poisoning Test is FAILED (%d failures found). !!!\n\n", failure); rc = 1; } return rc; } static void setup_log(void) { int rc = 0; if (clean_env) log_fd = fopen(log_file, "w"); else log_fd = fopen(log_file, "a"); if (!log_fd) err("cannot open log file: %s\n", log_file); if (clean_env) result_fd = fopen(result_file, "w"); else result_fd = fopen(result_file, "a"); if (!result_fd) err("cannot open log file: %s\n", result_file); if (tmp_dir[0] != '\0') { rc = mkdir(tmp_dir, 0777); if (rc && errno != EEXIST) err("cannot create tmp dir: %s: %s\n", tmp_dir, strerror(errno)); } } static void free_log(void) { fclose(log_fd); fclose(result_fd); } static void main_sighandler(int sig, siginfo_t * si, void *arg) { mylog("receive signal to get terminated\n"); cleanup(); exit(1); } static void setup_sig(void) { struct sigaction sa = { .sa_sigaction = main_sighandler, .sa_flags = SA_SIGINFO }; struct sigaction sa_bus = { .sa_sigaction = sighandler, .sa_flags = SA_SIGINFO }; sigaction(SIGINT, &sa, NULL); sigaction(SIGKILL, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGBUS, &sa_bus, NULL); } static void setup_test(void) { struct testcase *t; /* catch signals */ for (t = cases; t->f; t++) if (t->f == shm_test) t_shm = (t - cases); test_types = t - cases; } int main(int argc, char **argv) { int rc = 0, c, opt_index; snprintf(log_file, sizeof(log_file), "page-poisoning.log"); snprintf(result_file, sizeof(result_file), "page-poisoning.result"); snprintf(tmp_dir, sizeof(tmp_dir), "./tmp"); while (1) { c = getopt_long(argc, argv, "Chi:l:r:s:t:", opts, &opt_index); if (c == -1) break; switch (c) { case 'C': clean_env = 1; break; case 'h': help(); return 0; case 'i': child_num = strtol(optarg, NULL, 0); if (child_num > INSTANCE_NUM) child_num = INSTANCE_NUM; break; case 'l': snprintf(log_file, sizeof(log_file), "%s", optarg); break; case 'r': snprintf(result_file, sizeof(result_file), "%s", optarg); break; case 's': shm_size = strtol(optarg, NULL, 0); if (shm_size < SHM_SIZE) shm_size = SHM_SIZE; break; case 't': snprintf(tmp_dir, sizeof(tmp_dir), "%s", optarg); break; default: help(); return 0; } } if (!early_kill) system("sysctl -w vm.memory_failure_early_kill=0"); mypid = getpid(); setup_log(); setup_test(); if (!child_num) { mylog("end without test executed since child_num = 0\n"); return rc; } mylog("start page-poisoning test\n"); PS = getpagesize(); setup_ipc(); setup_sig(); rc = run_test(child_num); free_ipc(); mylog("page-poisoning test done!\n"); free_log(); return rc; }