git: 54808d50412a - releng/15.0 - cp: Fix copying the root directory

From: Colin Percival <cperciva_at_FreeBSD.org>
Date: Sun, 23 Nov 2025 16:39:10 UTC
The branch releng/15.0 has been updated by cperciva:

URL: https://cgit.FreeBSD.org/src/commit/?id=54808d50412aee31692f0041b4f78ddaf43bac8f

commit 54808d50412aee31692f0041b4f78ddaf43bac8f
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-11-22 12:11:59 +0000
Commit:     Colin Percival <cperciva@FreeBSD.org>
CommitDate: 2025-11-23 16:38:48 +0000

    cp: Fix copying the root directory
    
    When the source of the copy operation is the root directory, we should
    neither append it to the destination path on FTS_D nor trim it back off
    on FTS_DP.
    
    Approved by:    re (cperciva)
    PR:             291132
    MFC after:      3 days
    Fixes:          82fc0d09e862 ("cp: Partly restore symlink folllowing.")
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D53863
    
    (cherry picked from commit fe836c50120daed3e4084f43c27d8d650d36dee8)
    (cherry picked from commit c4843e5805dec10ee552d21f19c8da7707c1fcbc)
---
 bin/cp/cp.c             |  7 ++++++-
 bin/cp/tests/cp_test.sh | 15 +++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/bin/cp/cp.c b/bin/cp/cp.c
index 38fe65399d06..7ac1e5f6a4c4 100644
--- a/bin/cp/cp.c
+++ b/bin/cp/cp.c
@@ -433,6 +433,8 @@ copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
 				sep = strchr(to.base, '\0');
 				sep[0] = '/';
 				sep[1] = '\0';
+			} else if (strcmp(curr->fts_name, "/") == 0) {
+				/* special case when source is the root directory */
 			} else {
 				/* entering a directory; append its name to to.path */
 				len = snprintf(to.end, END(to.path) - to.end, "%s%s",
@@ -520,6 +522,8 @@ copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
 				if (type == DIR_TO_DNE &&
 				    curr->fts_level == FTS_ROOTLEVEL) {
 					/* this is actually our created root */
+				} else if (strcmp(curr->fts_name, "/") == 0) {
+					/* special case when source is the root directory */
 				} else {
 					while (to.end > to.path && *to.end != '/')
 						to.end--;
@@ -551,7 +555,8 @@ copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
 		/* Not an error but need to remember it happened. */
 		if (to.path[0] == '\0') {
 			/*
-			 * This can happen in two cases:
+			 * This can happen in three cases:
+			 * - The source path is the root directory.
 			 * - DIR_TO_DNE; we created the directory and
 			 *   populated root_stat earlier.
 			 * - FILE_TO_DIR if a source has a trailing slash;
diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index b637f862b7d3..af309ca7ea80 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -747,9 +747,23 @@ dstmode_body()
 	atf_check cmp dir/file dst/file
 }
 
+atf_test_case root
+root_head()
+{
+	atf_set "descr" "Test copying the root directory"
+}
+root_body()
+{
+	atf_check mkdir dst
+	atf_check -s exit:1 \
+	    -e inline:"cp: / is a directory (not copied).\n" \
+	    cp / dst
+}
+
 atf_test_case to_root cleanup
 to_root_head()
 {
+	atf_set "descr" "Test copying to the root directory"
 	atf_set "require.user" "unprivileged"
 }
 to_root_body()
@@ -893,6 +907,7 @@ atf_init_test_cases()
 	atf_add_test_case to_deaddirlink
 	atf_add_test_case to_link_outside
 	atf_add_test_case dstmode
+	atf_add_test_case root
 	atf_add_test_case to_root
 	atf_add_test_case dirloop
 	atf_add_test_case unrdir