#include <sys/stat.h>
#include <sys/time.h>
#include <errno.h>
#include <fcntl.h>
#include <fts.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <atf-c.h>
static volatile bool race_stop;
static void *
race_thrash(void *arg)
{
const char *path = arg;
while (!race_stop) {
(void)close(creat(path, 0644));
(void)unlink(path);
}
return (NULL);
}
static void *
dir_thrash(void *arg)
{
const char *path = arg;
while (!race_stop) {
(void)rmdir(path);
(void)mkdir(path, 0755);
}
return (NULL);
}
ATF_TC(read_no_exec_dir);
ATF_TC_HEAD(read_no_exec_dir, tc)
{
atf_tc_set_md_var(tc, "descr",
"directory with read but no execute is traversed via "
"FTS_DONTCHDIR fallback");
atf_tc_set_md_var(tc, "require.user", "unprivileged");
}
ATF_TC_BODY(read_no_exec_dir, tc)
{
char *paths[] = { "dir", NULL };
FTS *fts;
FTSENT *ent;
bool saw_d, saw_file;
ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
ATF_REQUIRE_EQ(0, chmod("dir", 0400));
ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
saw_d = false;
saw_file = false;
while ((ent = fts_read(fts)) != NULL) {
if (ent->fts_info == FTS_D &&
strcmp(ent->fts_name, "dir") == 0)
saw_d = true;
if (strcmp(ent->fts_name, "file") == 0)
saw_file = true;
}
ATF_CHECK_MSG(saw_d,
"FTS_D not returned for directory with mode 0400");
ATF_CHECK_MSG(saw_file,
"file inside mode 0400 directory was not visited");
ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
}
ATF_TC(no_slnone_for_nonsymlink);
ATF_TC_HEAD(no_slnone_for_nonsymlink, tc)
{
atf_tc_set_md_var(tc, "descr",
"FTS_SLNONE must not be returned for a non-symlink");
}
ATF_TC_BODY(no_slnone_for_nonsymlink, tc)
{
pthread_t thr;
char *paths[] = { "dir", NULL };
FTS *fts;
FTSENT *ent;
struct timespec start, now, elapsed;
ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
ATF_REQUIRE_EQ(0, symlink("nonexistent", "dir/dead"));
race_stop = false;
ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,
__DECONST(void *, "dir/victim")));
clock_gettime(CLOCK_MONOTONIC, &start);
for (;;) {
clock_gettime(CLOCK_MONOTONIC, &now);
timespecsub(&now, &start, &elapsed);
if (elapsed.tv_sec >= 1)
break;
fts = fts_open(paths, FTS_LOGICAL, NULL);
ATF_REQUIRE(fts != NULL);
while ((ent = fts_read(fts)) != NULL) {
if (ent->fts_info == FTS_SLNONE &&
ent->fts_statp->st_mode != 0 &&
!S_ISLNK(ent->fts_statp->st_mode))
ATF_CHECK_MSG(0,
"FTS_SLNONE returned for non-symlink '%s'",
ent->fts_name);
}
fts_close(fts);
}
race_stop = true;
pthread_join(thr, NULL);
}
ATF_TC(readdir_error_detected);
ATF_TC_HEAD(readdir_error_detected, tc)
{
atf_tc_set_md_var(tc, "descr",
"readdir errors produce FTS_DNR with fts_errno set");
atf_tc_set_md_var(tc, "require.user", "unprivileged");
}
ATF_TC_BODY(readdir_error_detected, tc)
{
char *paths[] = { "dir", NULL };
FTS *fts;
FTSENT *ent;
ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
ATF_REQUIRE_EQ(0, chmod("dir", 0100));
ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
ATF_REQUIRE((ent = fts_read(fts)) != NULL);
ATF_CHECK_EQ_MSG(FTS_D, ent->fts_info,
"expected FTS_D, got %d", ent->fts_info);
ATF_REQUIRE((ent = fts_read(fts)) != NULL);
ATF_CHECK_EQ_MSG(FTS_DNR, ent->fts_info,
"expected FTS_DNR, got %d", ent->fts_info);
ATF_CHECK_MSG(ent->fts_errno != 0,
"FTS_DNR must have non-zero fts_errno");
ATF_REQUIRE_EQ_MSG(NULL, fts_read(fts),
"expected NULL after FTS_DNR");
ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
}
ATF_TC(odirectory_changedir);
ATF_TC_HEAD(odirectory_changedir, tc)
{
atf_tc_set_md_var(tc, "descr",
"fts_safe_changedir handles concurrent dir/file substitution");
}
ATF_TC_BODY(odirectory_changedir, tc)
{
pthread_t thr;
char *paths[] = { "dir", NULL };
FTS *fts;
struct timespec start, now, elapsed;
ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
race_stop = false;
ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, dir_thrash,
__DECONST(void *, "dir/sub")));
clock_gettime(CLOCK_MONOTONIC, &start);
for (;;) {
clock_gettime(CLOCK_MONOTONIC, &now);
timespecsub(&now, &start, &elapsed);
if (elapsed.tv_sec >= 1)
break;
fts = fts_open(paths, FTS_PHYSICAL, NULL);
ATF_REQUIRE(fts != NULL);
while (fts_read(fts) != NULL)
;
fts_close(fts);
}
race_stop = true;
pthread_join(thr, NULL);
}
ATF_TC(concurrent_modification);
ATF_TC_HEAD(concurrent_modification, tc)
{
atf_tc_set_md_var(tc, "descr",
"no crash when tree modified during traversal");
}
ATF_TC_BODY(concurrent_modification, tc)
{
pthread_t thr;
char *paths[] = { "dir", NULL };
FTS *fts;
struct timespec start, now, elapsed;
ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
ATF_REQUIRE_EQ(0, close(creat("dir/stable", 0644)));
race_stop = false;
ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,
__DECONST(void *, "dir/victim")));
clock_gettime(CLOCK_MONOTONIC, &start);
for (;;) {
clock_gettime(CLOCK_MONOTONIC, &now);
timespecsub(&now, &start, &elapsed);
if (elapsed.tv_sec >= 1)
break;
fts = fts_open(paths, FTS_PHYSICAL, NULL);
ATF_REQUIRE(fts != NULL);
while (fts_read(fts) != NULL)
;
fts_close(fts);
}
race_stop = true;
pthread_join(thr, NULL);
}
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, read_no_exec_dir);
ATF_TP_ADD_TC(tp, no_slnone_for_nonsymlink);
ATF_TP_ADD_TC(tp, readdir_error_detected);
ATF_TP_ADD_TC(tp, odirectory_changedir);
ATF_TP_ADD_TC(tp, concurrent_modification);
return (atf_no_error());
}