The branch main has been updated by des:

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

commit eb439266b433241cec9cbef44328b16056bd6ff7
Author:     Dag-Erling Smørgrav <d...@freebsd.org>
AuthorDate: 2025-07-02 10:22:05 +0000
Commit:     Dag-Erling Smørgrav <d...@freebsd.org>
CommitDate: 2025-07-02 10:22:28 +0000

    cp: Don't rely on FTS_DP to keep track of depth.
    
    In normal operation, we get an FTS_D entry when we enter a directory
    and a matching FTS_DP entry when we leave it.  However, if an error
    occurs either changing to or reading a directory, we may get an FTS_D
    entry followed by FTS_DNR or even FTS_ERR instead.  Since FTS_ERR can
    also occur for non-directory entries, the only reliable way to keep
    track of when we leave a directory is to compare fts_level to our own
    depth counter.
    
    This fixes a rare assertion when attempting to recursively copy a
    directory tree containing a directory which is either not readable or
    not searchable.
    
    While here, also add a test case for directory loops.
    
    Fixes:          82fc0d09e8625
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D51096
---
 bin/cp/cp.c             | 54 ++++++++++++++++++++----------------
 bin/cp/tests/cp_test.sh | 73 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 104 insertions(+), 23 deletions(-)

diff --git a/bin/cp/cp.c b/bin/cp/cp.c
index 94a22c1cccc5..7e97715c3ef4 100644
--- a/bin/cp/cp.c
+++ b/bin/cp/cp.c
@@ -270,10 +270,9 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
        FTS *ftsp;
        FTSENT *curr;
        char *recpath = NULL, *sep;
-       int atflags, dne, badcp, len, rval;
+       int atflags, dne, badcp, len, level, rval;
        mode_t mask, mode;
        bool beneath = Rflag && type != FILE_TO_FILE;
-       bool skipdp = false;
 
        /*
         * Keep an inverted copy of the umask, for use in correcting
@@ -305,6 +304,7 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
                to.dir = -1;
        }
 
+       level = FTS_ROOTLEVEL;
        if ((ftsp = fts_open(argv, fts_options, NULL)) == NULL)
                err(1, "fts_open");
        for (badcp = rval = 0;
@@ -315,6 +315,20 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
                case FTS_NS:
                case FTS_DNR:
                case FTS_ERR:
+                       if (level > curr->fts_level) {
+                               /* leaving a directory; remove its name from 
to.path */
+                               if (type == DIR_TO_DNE &&
+                                   curr->fts_level == FTS_ROOTLEVEL) {
+                                       /* this is actually our created root */
+                               } else {
+                                       while (to.end > to.path && *to.end != 
'/')
+                                               to.end--;
+                                       assert(strcmp(to.end + (*to.end == '/'),
+                                           curr->fts_name) == 0);
+                                       *to.end = '\0';
+                               }
+                               level--;
+                       }
                        warnc(curr->fts_errno, "%s", curr->fts_path);
                        badcp = rval = 1;
                        continue;
@@ -335,14 +349,6 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
                                strlcpy(rootname, curr->fts_name,
                                    sizeof(rootname));
                        }
-                       /*
-                        * If we FTS_SKIP while handling FTS_D, we will
-                        * immediately get FTS_DP for the same directory.
-                        * If this happens before we've appended the name
-                        * to to.path, we need to remember not to perform
-                        * the reverse operation.
-                        */
-                       skipdp = true;
                        /* we must have a destination! */
                        if (type == DIR_TO_DNE &&
                            curr->fts_level == FTS_ROOTLEVEL) {
@@ -410,7 +416,7 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
                                }
                                to.end += len;
                        }
-                       skipdp = false;
+                       level++;
                        /*
                         * We're on the verge of recursing on ourselves.
                         * Either we need to stop right here (we knowingly
@@ -477,18 +483,19 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
                                        rval = 1;
                                }
                        }
-                       /* are we leaving a directory we failed to enter? */
-                       if (skipdp)
-                               continue;
-                       /* leaving a directory; remove its name from to.path */
-                       if (type == DIR_TO_DNE &&
-                           curr->fts_level == FTS_ROOTLEVEL) {
-                               /* this is actually our created root */
-                       } else {
-                               while (to.end > to.path && *to.end != '/')
-                                       to.end--;
-                               assert(strcmp(to.end + (*to.end == '/'), 
curr->fts_name) == 0);
-                               *to.end = '\0';
+                       if (level > curr->fts_level) {
+                               /* leaving a directory; remove its name from 
to.path */
+                               if (type == DIR_TO_DNE &&
+                                   curr->fts_level == FTS_ROOTLEVEL) {
+                                       /* this is actually our created root */
+                               } else {
+                                       while (to.end > to.path && *to.end != 
'/')
+                                               to.end--;
+                                       assert(strcmp(to.end + (*to.end == '/'),
+                                           curr->fts_name) == 0);
+                                       *to.end = '\0';
+                               }
+                               level--;
                        }
                        continue;
                default:
@@ -638,6 +645,7 @@ copy(char *argv[], enum op type, int fts_options, struct 
stat *root_stat)
                if (vflag && !badcp)
                        (void)printf("%s -> %s%s\n", curr->fts_path, to.base, 
to.path);
        }
+       assert(level == FTS_ROOTLEVEL);
        if (errno)
                err(1, "fts_read");
        (void)fts_close(ftsp);
diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index d5268ed4c4c9..64f917bf9c5f 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -618,6 +618,76 @@ to_root_cleanup()
        (dst=$(cat dst) && rm "/$dst") 2>/dev/null || true
 }
 
+atf_test_case dirloop
+dirloop_head()
+{
+       atf_set "descr" "Test cycle detection when recursing"
+}
+dirloop_body()
+{
+       mkdir -p src/a src/b
+       ln -s ../b src/a
+       ln -s ../a src/b
+       atf_check \
+           -s exit:1 \
+           -e match:"src/a/b/a: directory causes a cycle" \
+           -e match:"src/b/a/b: directory causes a cycle" \
+           cp -r src dst
+       atf_check test -d dst
+       atf_check test -d dst/a
+       atf_check test -d dst/b
+       atf_check test -d dst/a/b
+       atf_check test ! -e dst/a/b/a
+       atf_check test -d dst/b/a
+       atf_check test ! -e dst/b/a/b
+}
+
+atf_test_case unrdir
+unrdir_head()
+{
+       atf_set "descr" "Test handling of unreadable directories"
+}
+unrdir_body()
+{
+       for d in a b c ; do
+               mkdir -p src/$d
+               echo "$d" >src/$d/f
+       done
+       chmod 0 src/b
+       atf_check \
+           -s exit:1 \
+           -e match:"^cp: src/b: Permission denied" \
+           cp -R src dst
+       atf_check test -d dst/a
+       atf_check cmp src/a/f dst/a/f
+       atf_check test -d dst/b
+       atf_check test ! -e dst/b/f
+       atf_check test -d dst/c
+       atf_check cmp src/c/f dst/c/f
+}
+
+atf_test_case unrfile
+unrdir_head()
+{
+       atf_set "descr" "Test handling of unreadable files"
+}
+unrfile_body()
+{
+       mkdir src
+       for d in a b c ; do
+               echo "$d" >src/$d
+       done
+       chmod 0 src/b
+       atf_check \
+           -s exit:1 \
+           -e match:"^cp: src/b: Permission denied" \
+           cp -R src dst
+       atf_check test -d dst
+       atf_check cmp src/a dst/a
+       atf_check test ! -e dst/b
+       atf_check cmp src/c dst/c
+}
+
 atf_init_test_cases()
 {
        atf_add_test_case basic
@@ -656,4 +726,7 @@ atf_init_test_cases()
        atf_add_test_case to_link_outside
        atf_add_test_case dstmode
        atf_add_test_case to_root
+       atf_add_test_case dirloop
+       atf_add_test_case unrdir
+       atf_add_test_case unrfile
 }

Reply via email to