fts: add fts regression tests
Add ATF regression tests for previously-fixed fts(3) bugs: - PR 45723: directory with read but no execute is traversed via FTS_DONTCHDIR fallback, not silently skipped (commit1e03bff7f2) - PR 196724: FTS_SLNONE must not be returned for a non-symlink; time-bounded race test runs for 1 second with concurrent file creation/deletion (commit bf4374c54589) - PR 262038: readdir(2) errors produce FTS_DNR with fts_errno set, not silently treated as end-of-directory (commit0cff70ca66) - SVN r246641: normal traversal works correctly with O_DIRECTORY fix in fts_safe_changedir() (commit f9928f1705ee) - SVN r261589: no crash when tree modified during traversal; time-bounded race test runs for 1 second with concurrent file creation/deletion (commit c6d38f088e5c) Sponsored by: Google LLC (GSoC 2026) Reviewed by: asomers MFC after: 2 weeks Pull Request: https://github.com/freebsd/freebsd-src/pull/2257
This commit is contained in:
committed by
Alan Somers
parent
b45654c6a4
commit
670738a175
@@ -14,6 +14,7 @@ ATF_TESTS_C+= fts_children_test
|
|||||||
ATF_TESTS_C+= fts_misc_test
|
ATF_TESTS_C+= fts_misc_test
|
||||||
ATF_TESTS_C+= fts_open_test
|
ATF_TESTS_C+= fts_open_test
|
||||||
ATF_TESTS_C+= fts_options_test
|
ATF_TESTS_C+= fts_options_test
|
||||||
|
ATF_TESTS_C+= fts_regress_test
|
||||||
ATF_TESTS_C+= fts_set_test
|
ATF_TESTS_C+= fts_set_test
|
||||||
ATF_TESTS_C+= ftw_test
|
ATF_TESTS_C+= ftw_test
|
||||||
ATF_TESTS_C+= getentropy_test
|
ATF_TESTS_C+= getentropy_test
|
||||||
@@ -99,6 +100,7 @@ LIBADD.fpsetround_test+=m
|
|||||||
LIBADD.siginfo_test+= m
|
LIBADD.siginfo_test+= m
|
||||||
|
|
||||||
LIBADD.nice_test+= pthread
|
LIBADD.nice_test+= pthread
|
||||||
|
LIBADD.fts_regress_test+= pthread
|
||||||
LIBADD.syslog_test+= pthread
|
LIBADD.syslog_test+= pthread
|
||||||
|
|
||||||
CFLAGS+= -I${.CURDIR}
|
CFLAGS+= -I${.CURDIR}
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Jitendra Bhati
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Regression tests for specific FreeBSD bug reports fixed in fts(3).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Thrash function for file-based race tests: repeatedly creates and
|
||||||
|
* deletes a regular file at the given path.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Thrash function for directory-based race tests: repeatedly removes
|
||||||
|
* and recreates a directory at the given path.
|
||||||
|
*/
|
||||||
|
static void *
|
||||||
|
dir_thrash(void *arg)
|
||||||
|
{
|
||||||
|
const char *path = arg;
|
||||||
|
|
||||||
|
while (!race_stop) {
|
||||||
|
(void)rmdir(path);
|
||||||
|
(void)mkdir(path, 0755);
|
||||||
|
}
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PR 45723: A directory with read but no execute permission must be
|
||||||
|
* traversed. Before the fix, fts_build() gave up silently when
|
||||||
|
* chdir() failed, producing no output at all. The fix falls back to
|
||||||
|
* FTS_DONTCHDIR mode so the directory is still traversed using full
|
||||||
|
* relative paths.
|
||||||
|
*
|
||||||
|
* Requires an unprivileged user because root ignores permissions.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Before the fix, zero entries were produced. After the fix,
|
||||||
|
* fts falls back to FTS_DONTCHDIR and traverses using full paths.
|
||||||
|
* Verify the directory is not silently skipped.
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PR 196724: FTS_SLNONE must not be returned for a non-symlink.
|
||||||
|
*
|
||||||
|
* The fix ensures that FTS_SLNONE is only returned when lstat confirms
|
||||||
|
* the entry is actually a symlink. Exercised by a time-bounded race
|
||||||
|
* where a background thread creates and deletes a regular file while
|
||||||
|
* fts traverses with FTS_LOGICAL.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PR 262038: fts_build() must detect readdir(2) errors and not treat
|
||||||
|
* them as end-of-directory. The man page specifies that FTS_DNR must
|
||||||
|
* immediately follow FTS_D, in place of FTS_DP.
|
||||||
|
*
|
||||||
|
* Requires an unprivileged user because root ignores permissions.
|
||||||
|
*/
|
||||||
|
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)));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mode 0100: execute only, no read. chdir() succeeds but
|
||||||
|
* opendir/readdir fails. fts must return FTS_D then FTS_DNR
|
||||||
|
* (not FTS_DP) per the man page.
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SVN r246641: fts_safe_changedir() uses O_DIRECTORY to prevent a
|
||||||
|
* TOCTOU substitution attack where a directory is replaced with a
|
||||||
|
* non-directory between stat and open. Exercised by a time-bounded
|
||||||
|
* race where a background thread repeatedly removes and recreates
|
||||||
|
* dir/sub while fts traverses.
|
||||||
|
*/
|
||||||
|
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)));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Background thread races to remove and recreate dir/sub as a
|
||||||
|
* directory. With O_DIRECTORY the open fails safely if dir/sub
|
||||||
|
* is temporarily absent or replaced.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SVN r261589: fts must not double-free when the directory tree is
|
||||||
|
* concurrently modified. Exercised by a time-bounded race where a
|
||||||
|
* background thread creates and deletes a file during traversal.
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user