From nobody Wed Jul 2 10:23:01 2025 X-Original-To: dev-commits-src-all@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 4bXGF159JGz60D9V; Wed, 02 Jul 2025 10:23:01 +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 "R10" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4bXGF13nQ6z3KZY; Wed, 02 Jul 2025 10:23:01 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1751451781; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=Mi4QPZU13XWRyikz5t/BwgXxGkF2oFQast/jzWR+d10=; b=gvIDfybK3kWt8lDsIKwSVd62pVZeS72VIO+Pzc0j14IhZ6ZiOc/NnUAMC6BhWAURkkYlzn 8NC8/rMawlLMJRnCr8grHUKe7d5jiYITPGZx1ItJkT1lT0B6wsYDCiFnlFN8PkOb6W5o2i +W0wvcrLLEEuLENy4XNvz6Du8lxAkaHpPYoDEP9D8P6jQffS4mhlShehMZnlR+Kk/RsbwQ vjqHlKRPeNTUAN063QWoE66LOgRGdMtTVIqN8M+wUG8d5C0k3eg2IzKPLYupJmS4zksjVd 8zIRBsXpZhUlx3fDtrdFh6GR6jw79M6eAmwepBtp+jmA6eR65DD2ReGjZtk8bw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1751451781; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=Mi4QPZU13XWRyikz5t/BwgXxGkF2oFQast/jzWR+d10=; b=wufek0SfxcDjRsuxru4QdFhFAM5SrfA9cKJ0LYNZDvZoNs6W13ZAuDzJJtGpT4PRpcwoI+ pgcs8o/GGPN9ARqzzz9dz+B2XAHS5atKW26A1n80H4jMVBcQfm08TFccT//IbBtzXEjWSl VvRRJ0CyZhY3fuoXRXU+2wAz41XWTWY9KGokl/IrBkEn5dwuikZNtfjfsDCZdPKcWVsQXa g4ZC98EdYnbhcfpiJaEDcRtlg/VPe3p55nSmkQeyOlLQXHtTA0g4YQUW9vcfHegY5xzbVh 17faaMK7UFmYXGH3diLn7dhbpHn25qRBZIAQKi1tfMp5STreeqFu46H3uETsLA== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1751451781; a=rsa-sha256; cv=none; b=pBFOf2M2JrQR/Enotx3HNB42I4wdT4x9c8I/4Ti3mSwsU/+duZMapx3OeQc+qlBmnC8ZUt qdsfGsym8Cyo5KdB7TrBjmG7i3TWT71YByBhAvWSn6I/2+scPbLt3XbMQY83MG2QPAoGRT JgLga0bluGGfxYMRotznqiRXIEiS5UxOXtUDBy21qGRdj4aJy/IQEdPS5P8GGgr1ufsSg6 vxELD/tsXDwmeAH153GMIdZpCgC09RfDrMe02v1IM7aEcuZyUp2DndTdHpS3rixUvSAud3 R3gEyezVT0dyCrC2uzlIFuG4ayEgM7aVQ/FFLqQ+D4MA1gpq27v7CxT9fSZ6LQ== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (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 did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4bXGF139Hqz1YP; Wed, 02 Jul 2025 10:23:01 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.18.1/8.18.1) with ESMTP id 562AN17w050082; Wed, 2 Jul 2025 10:23:01 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.18.1/8.18.1/Submit) id 562AN1IR050079; Wed, 2 Jul 2025 10:23:01 GMT (envelope-from git) Date: Wed, 2 Jul 2025 10:23:01 GMT Message-Id: <202507021023.562AN1IR050079@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Dag-Erling =?utf-8?Q?Sm=C3=B8rgrav?= Subject: git: eb439266b433 - main - cp: Don't rely on FTS_DP to keep track of depth. List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: des X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: eb439266b433241cec9cbef44328b16056bd6ff7 Auto-Submitted: auto-generated The branch main has been updated by des: URL: https://cgit.FreeBSD.org/src/commit/?id=eb439266b433241cec9cbef44328b16056bd6ff7 commit eb439266b433241cec9cbef44328b16056bd6ff7 Author: Dag-Erling Smørgrav AuthorDate: 2025-07-02 10:22:05 +0000 Commit: Dag-Erling Smørgrav 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 }