Path: blob/main/lib/libc/tests/gen/fts_regress_test.c
289379 views
/*1* Copyright (c) 2026 Jitendra Bhati2*3* SPDX-License-Identifier: BSD-2-Clause4*/56/*7* Regression tests for specific FreeBSD bug reports fixed in fts(3).8*/910#include <sys/stat.h>11#include <sys/time.h>1213#include <errno.h>14#include <fcntl.h>15#include <fts.h>16#include <pthread.h>17#include <stdbool.h>18#include <stdio.h>19#include <stdlib.h>20#include <string.h>21#include <time.h>22#include <unistd.h>2324#include <atf-c.h>2526/*27* Thrash function for file-based race tests: repeatedly creates and28* deletes a regular file at the given path.29*/30static volatile bool race_stop;3132static void *33race_thrash(void *arg)34{35const char *path = arg;3637while (!race_stop) {38(void)close(creat(path, 0644));39(void)unlink(path);40}41return (NULL);42}4344/*45* Thrash function for directory-based race tests: repeatedly removes46* and recreates a directory at the given path.47*/48static void *49dir_thrash(void *arg)50{51const char *path = arg;5253while (!race_stop) {54(void)rmdir(path);55(void)mkdir(path, 0755);56}57return (NULL);58}5960/*61* PR 45723: A directory with read but no execute permission must be62* traversed. Before the fix, fts_build() gave up silently when63* chdir() failed, producing no output at all. The fix falls back to64* FTS_DONTCHDIR mode so the directory is still traversed using full65* relative paths.66*67* Requires an unprivileged user because root ignores permissions.68*/69ATF_TC(read_no_exec_dir);70ATF_TC_HEAD(read_no_exec_dir, tc)71{72atf_tc_set_md_var(tc, "descr",73"directory with read but no execute is traversed via "74"FTS_DONTCHDIR fallback");75atf_tc_set_md_var(tc, "require.user", "unprivileged");76}77ATF_TC_BODY(read_no_exec_dir, tc)78{79char *paths[] = { "dir", NULL };80FTS *fts;81FTSENT *ent;82bool saw_d, saw_file;8384ATF_REQUIRE_EQ(0, mkdir("dir", 0755));85ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));86ATF_REQUIRE_EQ(0, chmod("dir", 0400));8788ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);8990/*91* Before the fix, zero entries were produced. After the fix,92* fts falls back to FTS_DONTCHDIR and traverses using full paths.93* Verify the directory is not silently skipped.94*/95saw_d = false;96saw_file = false;97while ((ent = fts_read(fts)) != NULL) {98if (ent->fts_info == FTS_D &&99strcmp(ent->fts_name, "dir") == 0)100saw_d = true;101if (strcmp(ent->fts_name, "file") == 0)102saw_file = true;103}104105ATF_CHECK_MSG(saw_d,106"FTS_D not returned for directory with mode 0400");107ATF_CHECK_MSG(saw_file,108"file inside mode 0400 directory was not visited");109110ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");111}112113/*114* PR 196724: FTS_SLNONE must not be returned for a non-symlink.115*116* The fix ensures that FTS_SLNONE is only returned when lstat confirms117* the entry is actually a symlink. Exercised by a time-bounded race118* where a background thread creates and deletes a regular file while119* fts traverses with FTS_LOGICAL.120*/121ATF_TC(no_slnone_for_nonsymlink);122ATF_TC_HEAD(no_slnone_for_nonsymlink, tc)123{124atf_tc_set_md_var(tc, "descr",125"FTS_SLNONE must not be returned for a non-symlink");126}127ATF_TC_BODY(no_slnone_for_nonsymlink, tc)128{129pthread_t thr;130char *paths[] = { "dir", NULL };131FTS *fts;132FTSENT *ent;133struct timespec start, now, elapsed;134135ATF_REQUIRE_EQ(0, mkdir("dir", 0755));136ATF_REQUIRE_EQ(0, symlink("nonexistent", "dir/dead"));137138race_stop = false;139ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,140__DECONST(void *, "dir/victim")));141142clock_gettime(CLOCK_MONOTONIC, &start);143for (;;) {144clock_gettime(CLOCK_MONOTONIC, &now);145timespecsub(&now, &start, &elapsed);146if (elapsed.tv_sec >= 1)147break;148fts = fts_open(paths, FTS_LOGICAL, NULL);149ATF_REQUIRE(fts != NULL);150while ((ent = fts_read(fts)) != NULL) {151if (ent->fts_info == FTS_SLNONE &&152ent->fts_statp->st_mode != 0 &&153!S_ISLNK(ent->fts_statp->st_mode))154ATF_CHECK_MSG(0,155"FTS_SLNONE returned for non-symlink '%s'",156ent->fts_name);157}158fts_close(fts);159}160161race_stop = true;162pthread_join(thr, NULL);163}164165/*166* PR 262038: fts_build() must detect readdir(2) errors and not treat167* them as end-of-directory. The man page specifies that FTS_DNR must168* immediately follow FTS_D, in place of FTS_DP.169*170* Requires an unprivileged user because root ignores permissions.171*/172ATF_TC(readdir_error_detected);173ATF_TC_HEAD(readdir_error_detected, tc)174{175atf_tc_set_md_var(tc, "descr",176"readdir errors produce FTS_DNR with fts_errno set");177atf_tc_set_md_var(tc, "require.user", "unprivileged");178}179ATF_TC_BODY(readdir_error_detected, tc)180{181char *paths[] = { "dir", NULL };182FTS *fts;183FTSENT *ent;184185ATF_REQUIRE_EQ(0, mkdir("dir", 0755));186ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));187188/*189* Mode 0100: execute only, no read. chdir() succeeds but190* opendir/readdir fails. fts must return FTS_D then FTS_DNR191* (not FTS_DP) per the man page.192*/193ATF_REQUIRE_EQ(0, chmod("dir", 0100));194195ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);196197ATF_REQUIRE((ent = fts_read(fts)) != NULL);198ATF_CHECK_EQ_MSG(FTS_D, ent->fts_info,199"expected FTS_D, got %d", ent->fts_info);200201ATF_REQUIRE((ent = fts_read(fts)) != NULL);202ATF_CHECK_EQ_MSG(FTS_DNR, ent->fts_info,203"expected FTS_DNR, got %d", ent->fts_info);204ATF_CHECK_MSG(ent->fts_errno != 0,205"FTS_DNR must have non-zero fts_errno");206207ATF_REQUIRE_EQ_MSG(NULL, fts_read(fts),208"expected NULL after FTS_DNR");209210ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");211}212213/*214* SVN r246641: fts_safe_changedir() uses O_DIRECTORY to prevent a215* TOCTOU substitution attack where a directory is replaced with a216* non-directory between stat and open. Exercised by a time-bounded217* race where a background thread repeatedly removes and recreates218* dir/sub while fts traverses.219*/220ATF_TC(odirectory_changedir);221ATF_TC_HEAD(odirectory_changedir, tc)222{223atf_tc_set_md_var(tc, "descr",224"fts_safe_changedir handles concurrent dir/file substitution");225}226ATF_TC_BODY(odirectory_changedir, tc)227{228pthread_t thr;229char *paths[] = { "dir", NULL };230FTS *fts;231struct timespec start, now, elapsed;232233ATF_REQUIRE_EQ(0, mkdir("dir", 0755));234ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));235ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));236237/*238* Background thread races to remove and recreate dir/sub as a239* directory. With O_DIRECTORY the open fails safely if dir/sub240* is temporarily absent or replaced.241*/242race_stop = false;243ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, dir_thrash,244__DECONST(void *, "dir/sub")));245246clock_gettime(CLOCK_MONOTONIC, &start);247for (;;) {248clock_gettime(CLOCK_MONOTONIC, &now);249timespecsub(&now, &start, &elapsed);250if (elapsed.tv_sec >= 1)251break;252fts = fts_open(paths, FTS_PHYSICAL, NULL);253ATF_REQUIRE(fts != NULL);254while (fts_read(fts) != NULL)255;256fts_close(fts);257}258259race_stop = true;260pthread_join(thr, NULL);261}262263/*264* SVN r261589: fts must not double-free when the directory tree is265* concurrently modified. Exercised by a time-bounded race where a266* background thread creates and deletes a file during traversal.267*/268ATF_TC(concurrent_modification);269ATF_TC_HEAD(concurrent_modification, tc)270{271atf_tc_set_md_var(tc, "descr",272"no crash when tree modified during traversal");273}274ATF_TC_BODY(concurrent_modification, tc)275{276pthread_t thr;277char *paths[] = { "dir", NULL };278FTS *fts;279struct timespec start, now, elapsed;280281ATF_REQUIRE_EQ(0, mkdir("dir", 0755));282ATF_REQUIRE_EQ(0, close(creat("dir/stable", 0644)));283284race_stop = false;285ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,286__DECONST(void *, "dir/victim")));287288clock_gettime(CLOCK_MONOTONIC, &start);289for (;;) {290clock_gettime(CLOCK_MONOTONIC, &now);291timespecsub(&now, &start, &elapsed);292if (elapsed.tv_sec >= 1)293break;294fts = fts_open(paths, FTS_PHYSICAL, NULL);295ATF_REQUIRE(fts != NULL);296while (fts_read(fts) != NULL)297;298fts_close(fts);299}300301race_stop = true;302pthread_join(thr, NULL);303}304305ATF_TP_ADD_TCS(tp)306{307ATF_TP_ADD_TC(tp, read_no_exec_dir);308ATF_TP_ADD_TC(tp, no_slnone_for_nonsymlink);309ATF_TP_ADD_TC(tp, readdir_error_detected);310ATF_TP_ADD_TC(tp, odirectory_changedir);311ATF_TP_ADD_TC(tp, concurrent_modification);312313return (atf_no_error());314}315316317