blob: 0b392ac245c592066f20bf90bdbad214862aadea [file] [log] [blame]
/*
* 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 <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <setjmp.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>
#include <getopt.h>
#include <limits.h>
#include <sys/mman.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <sys/stat.h>
#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;
}