From nobody Fri Jun 5 20:05:41 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4gXC9Q68kXz6gQyL for ; Fri, 05 Jun 2026 20:05:46 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R13" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4gXC9Q5WvZz3KxD for ; Fri, 05 Jun 2026 20:05:46 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1780689946; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=AKBR1JkUDUrN6iyy1vnwsr5LbQXXv8E9hyLzTo8Oqlg=; b=u69QLY5zEYJ82jtxd0tXaC4/3Qj69tH9YS666VzXElBhBgb/OmBJ8+vju3Mb1Twc6pYQJL QrN5tNOrOuuJwK2P0VDbtdEBdBuBjWKmyUP+qyzzn6RdEa97YLk3QkLt4S+OxxTp5SQTq/ HcfwBTaIbyHaFTcUWsuBn32fWVN0TskJypbX5qECDJdkm3qchWRsuRPyOT7HV0A0oeI9Ev CZgw63Q4hOWd4xwUbTw3N7eDGt0bvaRXesdpTf9dV9nRsvP8zI6kYpuJBMPip0dyTJIKwv Z9EraFFB/Bhtpw2NlhqAsPPsjpaS+KcrgexqyX4g0ZUIJwWxUBtU3AVbew2iBA== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1780689946; a=rsa-sha256; cv=none; b=lUh7RdFvBSEBS7xmoKLhWPUt3Yzhdo3PYkCUmpG8Ac7NgLTF8M7ix6P6hX3V7Mz7JVodwi J/qHMUeoWrANBmMf7jKuqYfpYcRdvTnvF7+P3Infk04Y4/JwBBcYFIpZbSuh4eHlUZVlw/ hjIkUBSh1wyn3E84EcGL7aJti7zIsCI486ps9ByJEAZxy7tIbXPGNEmkWJ3dzvirsAsI+g l2XPNP7hcWxVXqpsifPJ838NcnSETmYpB9/jgjuwUeLjmaJA6wJaeWh5R/DUXwDmipfKAy AttVNBnLajaRgBnehCC9yOjcsf5Qgp1Xq3+XUpxCUnNuIyo9FDYEPtLI7UE01A== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1780689946; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=AKBR1JkUDUrN6iyy1vnwsr5LbQXXv8E9hyLzTo8Oqlg=; b=FA4fhdi3YpyWHql/CI7+GCW/t3LQKNsf6ZlHG92tJBpSVG7iW+J5kL4TUkVJZK7GGPGsxg uIQQql8ejl8S8GtrSsf22zrF6uVrjewaE8NJo82HT1QPX2fW/Oz6bMNcbYpIv2TDQih2RH wlukyd/gyCeHG6QtvlRNJrGZf8zubEM2qKiVeIEt09ENkLiEdHG9N9s9X4GEruztwjZJyl VLJzc5+wOXJa9pxwWHkO5byEPGAiJyJQC1iOcQ83ksiVP8xDZTspspU6fTGCOGfJ2dUKo1 T5Blgk+ERKs984lF6C3818RuTJ6TCzOzsuavNV0b8M5TwTTisitkgQWYPnTCzw== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4gXC9Q56YzzkfK for ; Fri, 05 Jun 2026 20:05:46 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 34a5f by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Fri, 05 Jun 2026 20:05:41 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: Jitendra Bhati From: Alan Somers Subject: git: 670738a17568 - main - fts: add fts regression tests List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: asomers X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 670738a17568f2579b866878f39d2a824a386297 Auto-Submitted: auto-generated Date: Fri, 05 Jun 2026 20:05:41 +0000 Message-Id: <6a232c15.34a5f.7a455a90@gitrepo.freebsd.org> The branch main has been updated by asomers: URL: https://cgit.FreeBSD.org/src/commit/?id=670738a17568f2579b866878f39d2a824a386297 commit 670738a17568f2579b866878f39d2a824a386297 Author: Jitendra Bhati AuthorDate: 2026-06-03 19:47:18 +0000 Commit: Alan Somers CommitDate: 2026-06-05 20:03:08 +0000 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 (commit 1e03bff7f2b7) - 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 (commit 0cff70ca6654) - 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 --- lib/libc/tests/gen/Makefile | 2 + lib/libc/tests/gen/fts_regress_test.c | 315 ++++++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+) diff --git a/lib/libc/tests/gen/Makefile b/lib/libc/tests/gen/Makefile index 7213fb4d4431..97b32827a66a 100644 --- a/lib/libc/tests/gen/Makefile +++ b/lib/libc/tests/gen/Makefile @@ -14,6 +14,7 @@ ATF_TESTS_C+= fts_children_test ATF_TESTS_C+= fts_misc_test ATF_TESTS_C+= fts_open_test ATF_TESTS_C+= fts_options_test +ATF_TESTS_C+= fts_regress_test ATF_TESTS_C+= fts_set_test ATF_TESTS_C+= ftw_test ATF_TESTS_C+= getentropy_test @@ -99,6 +100,7 @@ LIBADD.fpsetround_test+=m LIBADD.siginfo_test+= m LIBADD.nice_test+= pthread +LIBADD.fts_regress_test+= pthread LIBADD.syslog_test+= pthread CFLAGS+= -I${.CURDIR} diff --git a/lib/libc/tests/gen/fts_regress_test.c b/lib/libc/tests/gen/fts_regress_test.c new file mode 100644 index 000000000000..cf4035a65259 --- /dev/null +++ b/lib/libc/tests/gen/fts_regress_test.c @@ -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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* + * 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()); +}