1 /*
2 * Copyright (c) 2017 Richard Palethorpe <rpalethorpe@suse.com>
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 /*
18 * Perform a small read on every file in a directory tree.
19 *
20 * Useful for testing file systems like proc, sysfs and debugfs or anything
21 * which exposes a file like API so long as it respects O_NONBLOCK. This test
22 * is not concerned if a particular file in one of these file systems conforms
23 * exactly to its specific documented behavior. Just whether reading from that
24 * file causes a serious error such as a NULL pointer dereference.
25 *
26 * It is not required to run this as root, but test coverage will be much
27 * higher with full privileges.
28 *
29 * The reads are preformed by worker processes which are given file paths by a
30 * single parent process. The parent process recursively scans a given
31 * directory and passes the file paths it finds to the child processes using a
32 * queue structure stored in shared memory.
33 *
34 * This allows the file system and individual files to be accessed in
35 * parallel. Passing the 'reads' parameter (-r) will encourage this. The
36 * number of worker processes is based on the number of available
37 * processors. However this is limited by default to 15 to avoid this becoming
38 * an IPC stress test on systems with large numbers of weak cores. This can be
39 * overridden with the 'w' parameters.
40 */
41 #include <sys/types.h>
42 #include <sys/stat.h>
43 #include <lapi/fnmatch.h>
44 #include <stdlib.h>
45 #include <stdio.h>
46 #include <dirent.h>
47 #include <errno.h>
48 #include <unistd.h>
49 #include <string.h>
50 #include <limits.h>
51 #include <fnmatch.h>
52 #include <semaphore.h>
53 #include <ctype.h>
54 #include <pwd.h>
55 #include <grp.h>
56
57 #include "tst_test.h"
58
59 #define QUEUE_SIZE 16384
60 #define BUFFER_SIZE 1024
61 #define MAX_PATH 4096
62 #define MAX_DISPLAY 40
63
64 struct queue {
65 sem_t sem;
66 int front;
67 int back;
68 char data[QUEUE_SIZE];
69 };
70
71 struct worker {
72 pid_t pid;
73 struct queue *q;
74 };
75
76 enum dent_action {
77 DA_UNKNOWN,
78 DA_IGNORE,
79 DA_READ,
80 DA_VISIT,
81 };
82
83 static char *verbose;
84 static char *quiet;
85 static char *root_dir;
86 static char *exclude;
87 static char *str_reads;
88 static int reads = 1;
89 static char *str_worker_count;
90 static long worker_count;
91 static char *str_max_workers;
92 static long max_workers = 15;
93 static struct worker *workers;
94 static char *drop_privs;
95
96 static struct tst_option options[] = {
97 {"v", &verbose,
98 "-v Print information about successful reads."},
99 {"q", &quiet,
100 "-q Don't print file read or open errors."},
101 {"d:", &root_dir,
102 "-d path Path to the directory to read from, defaults to /sys."},
103 {"e:", &exclude,
104 "-e pattern Ignore files which match an 'extended' pattern, see fnmatch(3)."},
105 {"r:", &str_reads,
106 "-r count The number of times to schedule a file for reading."},
107 {"w:", &str_max_workers,
108 "-w count Set the worker count limit, the default is 15."},
109 {"W:", &str_worker_count,
110 "-W count Override the worker count. Ignores (-w) and the processor count."},
111 {"p", &drop_privs,
112 "-p Drop privileges; switch to the nobody user."},
113 {NULL, NULL, NULL}
114 };
115
queue_pop(struct queue * q,char * buf)116 static int queue_pop(struct queue *q, char *buf)
117 {
118 int i = q->front, j = 0;
119
120 sem_wait(&q->sem);
121
122 if (!q->data[i])
123 return 0;
124
125 while (q->data[i]) {
126 buf[j] = q->data[i];
127
128 if (++j >= BUFFER_SIZE - 1)
129 tst_brk(TBROK, "Buffer is too small for path");
130 if (++i >= QUEUE_SIZE)
131 i = 0;
132 }
133
134 buf[j] = '\0';
135 tst_atomic_store(i + 1, &q->front);
136
137 return 1;
138 }
139
queue_push(struct queue * q,const char * buf)140 static int queue_push(struct queue *q, const char *buf)
141 {
142 int i = q->back, j = 0;
143 int front = tst_atomic_load(&q->front);
144
145 do {
146 q->data[i] = buf[j];
147
148 if (++i >= QUEUE_SIZE)
149 i = 0;
150 if (i == front)
151 return 0;
152
153 } while (buf[j++]);
154
155 q->back = i;
156 sem_post(&q->sem);
157
158 return 1;
159 }
160
queue_init(void)161 static struct queue *queue_init(void)
162 {
163 struct queue *q = SAFE_MMAP(NULL, sizeof(*q),
164 PROT_READ | PROT_WRITE,
165 MAP_SHARED | MAP_ANONYMOUS,
166 0, 0);
167
168 sem_init(&q->sem, 1, 0);
169 q->front = 0;
170 q->back = 0;
171
172 return q;
173 }
174
queue_destroy(struct queue * q,int is_worker)175 static void queue_destroy(struct queue *q, int is_worker)
176 {
177 if (is_worker)
178 sem_destroy(&q->sem);
179
180 SAFE_MUNMAP(q, sizeof(*q));
181 }
182
sanitize_str(char * buf,ssize_t count)183 static void sanitize_str(char *buf, ssize_t count)
184 {
185 int i;
186
187 for (i = 0; i < MIN(count, MAX_DISPLAY); i++)
188 if (!isprint(buf[i]))
189 buf[i] = ' ';
190
191 if (count <= MAX_DISPLAY)
192 buf[count] = '\0';
193 else
194 strcpy(buf + MAX_DISPLAY, "...");
195 }
196
read_test(const char * path)197 static void read_test(const char *path)
198 {
199 char buf[BUFFER_SIZE];
200 int fd;
201 ssize_t count;
202
203 if (exclude && !fnmatch(exclude, path, FNM_EXTMATCH)) {
204 if (verbose)
205 tst_res(TINFO, "Ignoring %s", path);
206 return;
207 }
208
209 if (verbose)
210 tst_res(TINFO, "%s(%s)", __func__, path);
211
212 fd = open(path, O_RDONLY | O_NONBLOCK);
213 if (fd < 0) {
214 if (!quiet)
215 tst_res(TINFO | TERRNO, "open(%s)", path);
216 return;
217 }
218
219 count = read(fd, buf, sizeof(buf) - 1);
220 if (count > 0 && verbose) {
221 sanitize_str(buf, count);
222 tst_res(TINFO, "read(%s, buf) = %zi, buf = %s",
223 path, count, buf);
224 } else if (!count && verbose) {
225 tst_res(TINFO, "read(%s) = EOF", path);
226 } else if (count < 0 && !quiet) {
227 tst_res(TINFO | TERRNO, "read(%s)", path);
228 }
229
230 SAFE_CLOSE(fd);
231 }
232
worker_run(struct worker * self)233 static int worker_run(struct worker *self)
234 {
235 char buf[BUFFER_SIZE];
236 struct sigaction term_sa = {
237 .sa_handler = SIG_IGN,
238 .sa_flags = 0,
239 };
240 struct queue *q = self->q;
241
242 sigaction(SIGTTIN, &term_sa, NULL);
243
244 while (1) {
245 if (!queue_pop(q, buf))
246 break;
247
248 read_test(buf);
249 }
250
251 queue_destroy(q, 1);
252 tst_flush();
253 return 0;
254 }
255
maybe_drop_privs(void)256 static void maybe_drop_privs(void)
257 {
258 struct passwd *nobody;
259
260 if (!drop_privs)
261 return;
262
263 TEST(setgroups(0, NULL));
264 if (TST_RET < 0 && TST_ERR != EPERM) {
265 tst_brk(TBROK | TTERRNO,
266 "Failed to clear suplementary group set");
267 }
268
269 nobody = SAFE_GETPWNAM("nobody");
270
271 TEST(setgid(nobody->pw_gid));
272 if (TST_RET < 0 && TST_ERR != EPERM)
273 tst_brk(TBROK | TTERRNO, "Failed to use nobody gid");
274
275 TEST(setuid(nobody->pw_uid));
276 if (TST_RET < 0 && TST_ERR != EPERM)
277 tst_brk(TBROK | TTERRNO, "Failed to use nobody uid");
278 }
279
spawn_workers(void)280 static void spawn_workers(void)
281 {
282 int i;
283 struct worker *wa = workers;
284
285 bzero(workers, worker_count * sizeof(*workers));
286
287 for (i = 0; i < worker_count; i++) {
288 wa[i].q = queue_init();
289 wa[i].pid = SAFE_FORK();
290 if (!wa[i].pid) {
291 maybe_drop_privs();
292 exit(worker_run(wa + i));
293 }
294 }
295 }
296
stop_workers(void)297 static void stop_workers(void)
298 {
299 const char stop_code[1] = { '\0' };
300 int i;
301
302 if (!workers)
303 return;
304
305 for (i = 0; i < worker_count; i++) {
306 if (workers[i].q)
307 TST_RETRY_FUNC(queue_push(workers[i].q, stop_code), 1);
308 }
309
310 for (i = 0; i < worker_count; i++) {
311 if (workers[i].q) {
312 queue_destroy(workers[i].q, 0);
313 workers[i].q = 0;
314 }
315 }
316 }
317
rep_sched_work(const char * path,int rep)318 static void rep_sched_work(const char *path, int rep)
319 {
320 int i, j;
321
322 for (i = j = 0; i < rep; i++, j++) {
323 if (j >= worker_count)
324 j = 0;
325 TST_RETRY_FUNC(queue_push(workers[j].q, path), 1);
326 }
327 }
328
setup(void)329 static void setup(void)
330 {
331 if (tst_parse_int(str_reads, &reads, 1, INT_MAX))
332 tst_brk(TBROK,
333 "Invalid reads (-r) argument: '%s'", str_reads);
334
335 if (tst_parse_long(str_max_workers, &max_workers, 1, LONG_MAX)) {
336 tst_brk(TBROK,
337 "Invalid max workers (-w) argument: '%s'",
338 str_max_workers);
339 }
340
341 if (tst_parse_long(str_worker_count, &worker_count, 1, LONG_MAX)) {
342 tst_brk(TBROK,
343 "Invalid worker count (-W) argument: '%s'",
344 str_worker_count);
345 }
346
347 if (!root_dir)
348 tst_brk(TBROK, "The directory argument (-d) is required");
349
350 if (!worker_count)
351 worker_count = MIN(MAX(tst_ncpus() - 1, 1), max_workers);
352 workers = SAFE_MALLOC(worker_count * sizeof(*workers));
353 }
354
cleanup(void)355 static void cleanup(void)
356 {
357 stop_workers();
358 free(workers);
359 }
360
visit_dir(const char * path)361 static void visit_dir(const char *path)
362 {
363 DIR *dir;
364 struct dirent *dent;
365 struct stat dent_st;
366 char dent_path[MAX_PATH];
367 enum dent_action act;
368
369 dir = opendir(path);
370 if (!dir) {
371 tst_res(TINFO | TERRNO, "opendir(%s)", path);
372 return;
373 }
374
375 while (1) {
376 errno = 0;
377 dent = readdir(dir);
378 if (!dent && errno) {
379 tst_res(TINFO | TERRNO, "readdir(%s)", path);
380 break;
381 } else if (!dent) {
382 break;
383 }
384
385 if (!strcmp(dent->d_name, ".") ||
386 !strcmp(dent->d_name, ".."))
387 continue;
388
389 if (dent->d_type == DT_DIR)
390 act = DA_VISIT;
391 else if (dent->d_type == DT_LNK)
392 act = DA_IGNORE;
393 else if (dent->d_type == DT_UNKNOWN)
394 act = DA_UNKNOWN;
395 else
396 act = DA_READ;
397
398 snprintf(dent_path, MAX_PATH,
399 "%s/%s", path, dent->d_name);
400
401 if (act == DA_UNKNOWN) {
402 if (lstat(dent_path, &dent_st))
403 tst_res(TINFO | TERRNO, "lstat(%s)", path);
404 else if ((dent_st.st_mode & S_IFMT) == S_IFDIR)
405 act = DA_VISIT;
406 else if ((dent_st.st_mode & S_IFMT) == S_IFLNK)
407 act = DA_IGNORE;
408 else
409 act = DA_READ;
410 }
411
412 if (act == DA_VISIT)
413 visit_dir(dent_path);
414 else if (act == DA_READ)
415 rep_sched_work(dent_path, reads);
416 }
417
418 if (closedir(dir))
419 tst_res(TINFO | TERRNO, "closedir(%s)", path);
420 }
421
run(void)422 static void run(void)
423 {
424 spawn_workers();
425 visit_dir(root_dir);
426 stop_workers();
427
428 tst_reap_children();
429 tst_res(TPASS, "Finished reading files");
430 }
431
432 static struct tst_test test = {
433 .options = options,
434 .setup = setup,
435 .cleanup = cleanup,
436 .test_all = run,
437 .forks_child = 1,
438 };
439
440