android13/external/ltp/testcases/kernel/fs/read_all/read_all.c

476 lines
9.7 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (c) 2017 Richard Palethorpe <rpalethorpe@suse.com>
*/
/*
* Perform a small read on every file in a directory tree.
*
* Useful for testing file systems like proc, sysfs and debugfs or anything
* which exposes a file like API so long as it respects O_NONBLOCK. This test
* is not concerned if a particular file in one of these file systems conforms
* exactly to its specific documented behavior. Just whether reading from that
* file causes a serious error such as a NULL pointer dereference.
*
* It is not required to run this as root, but test coverage will be much
* higher with full privileges.
*
* The reads are preformed by worker processes which are given file paths by a
* single parent process. The parent process recursively scans a given
* directory and passes the file paths it finds to the child processes using a
* queue structure stored in shared memory.
*
* This allows the file system and individual files to be accessed in
* parallel. Passing the 'reads' parameter (-r) will encourage this. The
* number of worker processes is based on the number of available
* processors. However this is limited by default to 15 to avoid this becoming
* an IPC stress test on systems with large numbers of weak cores. This can be
* overridden with the 'w' parameters.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <lapi/fnmatch.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <limits.h>
#include <fnmatch.h>
#include <semaphore.h>
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#include "tst_test.h"
#define QUEUE_SIZE 16384
#define BUFFER_SIZE 1024
#define MAX_PATH 4096
#define MAX_DISPLAY 40
struct queue {
sem_t sem;
int front;
int back;
char data[QUEUE_SIZE];
};
struct worker {
pid_t pid;
struct queue *q;
};
enum dent_action {
DA_UNKNOWN,
DA_IGNORE,
DA_READ,
DA_VISIT,
};
static char *verbose;
static char *quiet;
static char *root_dir;
static char *str_reads;
static int reads = 1;
static char *str_worker_count;
static long worker_count;
static char *str_max_workers;
static long max_workers = 15;
static struct worker *workers;
static char *drop_privs;
static char *blacklist[] = {
NULL, /* reserved for -e parameter */
"/sys/power/wakeup_count",
"/sys/kernel/debug/*",
"/sys/devices/platform/*/eeprom",
"/sys/devices/platform/*/nvmem",
"/sys/*/cpu??*(?)/*", /* cpu* entries with 2 or more digits */
};
static int queue_pop(struct queue *q, char *buf)
{
int i = q->front, j = 0;
sem_wait(&q->sem);
if (!q->data[i])
return 0;
while (q->data[i]) {
buf[j] = q->data[i];
if (++j >= BUFFER_SIZE - 1)
tst_brk(TBROK, "Buffer is too small for path");
i = (i + 1) % QUEUE_SIZE;
}
buf[j] = '\0';
tst_atomic_store((i + 1) % QUEUE_SIZE, &q->front);
return 1;
}
static int queue_push(struct queue *q, const char *buf)
{
int i = q->back, j = 0;
int front = tst_atomic_load(&q->front);
do {
q->data[i] = buf[j];
i = (i + 1) % QUEUE_SIZE;
if (i == front)
return 0;
} while (buf[j++]);
q->back = i;
sem_post(&q->sem);
return 1;
}
static struct queue *queue_init(void)
{
struct queue *q = SAFE_MMAP(NULL, sizeof(*q),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
0, 0);
sem_init(&q->sem, 1, 0);
q->front = 0;
q->back = 0;
return q;
}
static void queue_destroy(struct queue *q, int is_worker)
{
if (is_worker)
sem_destroy(&q->sem);
SAFE_MUNMAP(q, sizeof(*q));
}
static void sanitize_str(char *buf, ssize_t count)
{
int i;
for (i = 0; i < MIN(count, MAX_DISPLAY); i++)
if (!isprint(buf[i]))
buf[i] = ' ';
if (count <= MAX_DISPLAY)
buf[count] = '\0';
else
strcpy(buf + MAX_DISPLAY, "...");
}
static int is_blacklisted(const char *path)
{
unsigned int i;
for (i = 0; i < ARRAY_SIZE(blacklist); i++) {
if (blacklist[i] && !fnmatch(blacklist[i], path, FNM_EXTMATCH)) {
if (verbose)
tst_res(TINFO, "Ignoring %s", path);
return 1;
}
}
return 0;
}
static void read_test(const char *path)
{
char buf[BUFFER_SIZE];
int fd;
ssize_t count;
if (is_blacklisted(path))
return;
if (verbose)
tst_res(TINFO, "%s(%s)", __func__, path);
fd = open(path, O_RDONLY | O_NONBLOCK);
if (fd < 0) {
if (!quiet)
tst_res(TINFO | TERRNO, "open(%s)", path);
return;
}
count = read(fd, buf, sizeof(buf) - 1);
if (count > 0 && verbose) {
sanitize_str(buf, count);
tst_res(TINFO, "read(%s, buf) = %zi, buf = %s",
path, count, buf);
} else if (!count && verbose) {
tst_res(TINFO, "read(%s) = EOF", path);
} else if (count < 0 && !quiet) {
tst_res(TINFO | TERRNO, "read(%s)", path);
}
SAFE_CLOSE(fd);
}
static int worker_run(struct worker *self)
{
char buf[BUFFER_SIZE];
struct sigaction term_sa = {
.sa_handler = SIG_IGN,
.sa_flags = 0,
};
struct queue *q = self->q;
sigaction(SIGTTIN, &term_sa, NULL);
while (1) {
if (!queue_pop(q, buf))
break;
read_test(buf);
}
queue_destroy(q, 1);
tst_flush();
return 0;
}
static void maybe_drop_privs(void)
{
struct passwd *nobody;
if (!drop_privs)
return;
TEST(setgroups(0, NULL));
if (TST_RET < 0 && TST_ERR != EPERM) {
tst_brk(TBROK | TTERRNO,
"Failed to clear suplementary group set");
}
nobody = SAFE_GETPWNAM("nobody");
TEST(setgid(nobody->pw_gid));
if (TST_RET < 0 && TST_ERR != EPERM)
tst_brk(TBROK | TTERRNO, "Failed to use nobody gid");
TEST(setuid(nobody->pw_uid));
if (TST_RET < 0 && TST_ERR != EPERM)
tst_brk(TBROK | TTERRNO, "Failed to use nobody uid");
}
static void spawn_workers(void)
{
int i;
struct worker *wa = workers;
memset(workers, 0, worker_count * sizeof(*workers));
for (i = 0; i < worker_count; i++) {
wa[i].q = queue_init();
wa[i].pid = SAFE_FORK();
if (!wa[i].pid) {
maybe_drop_privs();
exit(worker_run(wa + i));
}
}
}
static void work_push_retry(int worker, const char *buf)
{
int i, ret, worker_min, worker_max, usleep_time = 100;
if (worker < 0) {
/* pick any, try -worker first */
worker_min = worker * (-1);
worker_max = worker_count;
} else {
/* keep trying worker */
worker_min = worker;
worker_max = worker + 1;
}
i = worker_min;
for (;;) {
ret = queue_push(workers[i].q, buf);
if (ret == 1)
break;
if (++i >= worker_max) {
i = worker_min;
if (usleep_time < 100000)
usleep_time *= 2;
usleep(usleep_time);
}
}
}
static void stop_workers(void)
{
const char stop_code[1] = { '\0' };
int i;
if (!workers)
return;
for (i = 0; i < worker_count; i++) {
if (workers[i].q)
work_push_retry(i, stop_code);
}
for (i = 0; i < worker_count; i++) {
if (workers[i].q) {
queue_destroy(workers[i].q, 0);
workers[i].q = 0;
}
}
}
static void rep_sched_work(const char *path, int rep)
{
int i, j;
for (i = j = 0; i < rep; i++, j++) {
if (j >= worker_count)
j = 0;
work_push_retry(-j, path);
}
}
static void setup(void)
{
if (tst_parse_int(str_reads, &reads, 1, INT_MAX))
tst_brk(TBROK,
"Invalid reads (-r) argument: '%s'", str_reads);
if (tst_parse_long(str_max_workers, &max_workers, 1, LONG_MAX)) {
tst_brk(TBROK,
"Invalid max workers (-w) argument: '%s'",
str_max_workers);
}
if (tst_parse_long(str_worker_count, &worker_count, 1, LONG_MAX)) {
tst_brk(TBROK,
"Invalid worker count (-W) argument: '%s'",
str_worker_count);
}
if (!root_dir)
tst_brk(TBROK, "The directory argument (-d) is required");
if (!worker_count)
worker_count = MIN(MAX(tst_ncpus() - 1, 1), max_workers);
workers = SAFE_MALLOC(worker_count * sizeof(*workers));
}
static void cleanup(void)
{
stop_workers();
free(workers);
}
static void visit_dir(const char *path)
{
DIR *dir;
struct dirent *dent;
struct stat dent_st;
char dent_path[MAX_PATH];
enum dent_action act;
dir = opendir(path);
if (!dir) {
tst_res(TINFO | TERRNO, "opendir(%s)", path);
return;
}
while (1) {
errno = 0;
dent = readdir(dir);
if (!dent && errno) {
tst_res(TINFO | TERRNO, "readdir(%s)", path);
break;
} else if (!dent) {
break;
}
if (!strcmp(dent->d_name, ".") ||
!strcmp(dent->d_name, ".."))
continue;
if (dent->d_type == DT_DIR)
act = DA_VISIT;
else if (dent->d_type == DT_LNK)
act = DA_IGNORE;
else if (dent->d_type == DT_UNKNOWN)
act = DA_UNKNOWN;
else
act = DA_READ;
snprintf(dent_path, MAX_PATH,
"%s/%s", path, dent->d_name);
if (act == DA_UNKNOWN) {
if (lstat(dent_path, &dent_st))
tst_res(TINFO | TERRNO, "lstat(%s)", path);
else if ((dent_st.st_mode & S_IFMT) == S_IFDIR)
act = DA_VISIT;
else if ((dent_st.st_mode & S_IFMT) == S_IFLNK)
act = DA_IGNORE;
else
act = DA_READ;
}
if (act == DA_VISIT)
visit_dir(dent_path);
else if (act == DA_READ)
rep_sched_work(dent_path, reads);
}
if (closedir(dir))
tst_res(TINFO | TERRNO, "closedir(%s)", path);
}
static void run(void)
{
spawn_workers();
visit_dir(root_dir);
stop_workers();
tst_reap_children();
tst_res(TPASS, "Finished reading files");
}
static struct tst_test test = {
.options = (struct tst_option[]) {
{"v", &verbose,
"-v Print information about successful reads."},
{"q", &quiet,
"-q Don't print file read or open errors."},
{"d:", &root_dir,
"-d path Path to the directory to read from, defaults to /sys."},
{"e:", &blacklist[0],
"-e pattern Ignore files which match an 'extended' pattern, see fnmatch(3)."},
{"r:", &str_reads,
"-r count The number of times to schedule a file for reading."},
{"w:", &str_max_workers,
"-w count Set the worker count limit, the default is 15."},
{"W:", &str_worker_count,
"-W count Override the worker count. Ignores (-w) and the processor count."},
{"p", &drop_privs,
"-p Drop privileges; switch to the nobody user."},
{}
},
.setup = setup,
.cleanup = cleanup,
.test_all = run,
.forks_child = 1,
};