git: 537fbf70f12b - main - cp: Fix copying to root directory.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Fri, 30 May 2025 15:44:55 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=537fbf70f12b014d68fc087ee6e254523c73d489

commit 537fbf70f12b014d68fc087ee6e254523c73d489
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-05-30 15:44:20 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-05-30 15:44:39 +0000

    cp: Fix copying to root directory.
    
    The trailing-slash logic would strip the target down to an empty
    string, which we would then fail to stat, and subsequently attempt
    (and fail) to create.
    
    Fixes:          82fc0d09e862
    Sponsored by:   Klara, Inc.
    Reviewed by:    allanjude
    Differential Revision:  https://reviews.freebsd.org/D50604
---
 bin/cp/cp.c             | 12 +++++++-----
 bin/cp/tests/cp_test.sh | 22 ++++++++++++++++++++++
 2 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/bin/cp/cp.c b/bin/cp/cp.c
index 03b3a7a7bf41..94a22c1cccc5 100644
--- a/bin/cp/cp.c
+++ b/bin/cp/cp.c
@@ -179,9 +179,9 @@ main(int argc, char *argv[])
 		target = dot;
 	} else if ((sep = strrchr(target, '/')) != NULL && sep[1] == '\0') {
 		have_trailing_slash = true;
-		while (sep > target + 1 && *(sep - 1) == '/')
+		while (sep > target && *sep == '/')
 			sep--;
-		*sep = '\0';
+		sep[1] = '\0';
 	}
 	/*
 	 * Copy target into to.base, leaving room for a possible separator
@@ -293,9 +293,11 @@ copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
 		/*
 		 * We have previously made sure there is room for this.
 		 */
-		sep = strchr(to.base, '\0');
-		sep[0] = '/';
-		sep[1] = '\0';
+		if (strcmp(to.base, "/") != 0) {
+			sep = strchr(to.base, '\0');
+			sep[0] = '/';
+			sep[1] = '\0';
+		}
 	} else {
 		/*
 		 * We will create the destination directory imminently.
diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index bb45cc2484f5..8511904a0e33 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -570,6 +570,27 @@ dstmode_body()
 	atf_check cmp dir/file dst/file
 }
 
+atf_test_case to_root cleanup
+to_root_head()
+{
+	atf_set "require.user" "root"
+}
+to_root_body()
+{
+	dst="$(atf_get ident).$$"
+	echo "$dst" >dst
+	echo "foo" >"$dst"
+	atf_check cp "$dst" /
+	atf_check cmp -s "$dst" "/$dst"
+	atf_check rm "/$dst"
+	atf_check cp "$dst" //
+	atf_check cmp -s "$dst" "/$dst"
+}
+to_root_cleanup()
+{
+	(dst=$(cat dst) && [ -n "/$dst" ] && [ -f "/$dst" ] && rm "/$dst") || true
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case basic
@@ -607,4 +628,5 @@ 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 to_root
 }