commit:     4521e440ba1760165e6fae3b3ef2d0b7673689a0
Author:     Kerin Millar <kfm <AT> plushkava <DOT> net>
AuthorDate: Sun Jul 13 10:03:52 2025 +0000
Commit:     Sam James <sam <AT> gentoo <DOT> org>
CommitDate: Tue Jul 22 22:25:38 2025 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=4521e440

phase-helpers.sh: have eapply_user() abort if a given patch dir can't be read

Presently, the eapply_user() function is responsible for applying
user-supplied patches for EAPIs that support this feature (6 or
greater). In order to collect the patches, it walks a set of pathnames
that may exist as directories relative to the "/etc/portage/patches"
directory. However, it is unable to handle various modes of failure.
Consider the following potential scenarios.

- the pathname cannot be opened as a directory e.g. due to EACCESS
- the pathname is something other than a directory
- the pathname is a dangling symlink

In each of these scenarios, portage raises no diagnostic messages, nor
does it abort. Consequently, patches that the user relies upon being
applied might not actually be applied. Further, patches that the user
relies upon being suppressed - by way of an overlapping patch of greater
specificity that is an empty regular file - might not actually be
suppressed. To make matters worse, it is possible for the user to be
initially unaware that anything is wrong, particularly if specifying the
--quiet-build and/or --jobs options to emerge(1). Further, upon
discovering that there is a problem, they may lack the skills required
to perform a rapid diagnosis, duly placing a support burden upon others.

Address this issue by collecting the patches in such a way that portage
is able to determine whether a given pathname exists and can be read as
a directory. To do so in bash is challenging because it cannot report
the error numbers of syscalls such as opendir(3) and readdir(3) after
attempting pathname expansion. This commit incorporates a new function
by the name of __readdir(). Given the ostensible pathname of a
directory, it tries to collect the names of its immediate entries into
an array variable named 'dirents'. It works on the basis that, for both
the . and .. entries to be encountered, it can be reasonably assumed
that the pathname was successfully opened and read as a directory.

Hence, the eapply_user() function now calls __readdir(). Should the
latter function return false, the former function will test whether the
pathname exists and, if it does, convey a meaningful diagnostic message
to the __helpers_die() function. The reason for using __helpers_die()
over die() is to ensure that the implementation continues to conform
with the following excerpt from the Package Manager Specification.

"Returns shell true (0) if patches applied successfully, or if no
patches were provided. Otherwise, aborts the build process, unless run
using nonfatal."

Consider the following test case, where a patch directory is created for
the sys-apps/pv package with an unduly restrictive mode.

# mkdir -p /etc/portage/patches/sys-apps/pv
# chmod 700 /etc/portage/patches/sys-apps/pv
# emerge pv

The resulting exception shall be as follows.

 * ERROR: sys-apps/pv-1.9.31::gentoo failed (prepare phase):
 *   eapply_user: '/etc/portage/patches/sys-apps/pv' exists but can't be
 opened as a directory by 'portage'

Bug: https://bugs.gentoo.org/634396
Link: https://projects.gentoo.org/pms/8/pms.html#x1-12700012.3.7
Signed-off-by: Kerin Millar <kfm <AT> plushkava.net>
Signed-off-by: Sam James <sam <AT> gentoo.org>

 bin/phase-helpers.sh | 39 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/bin/phase-helpers.sh b/bin/phase-helpers.sh
index 5811a90658..0759affb0b 100644
--- a/bin/phase-helpers.sh
+++ b/bin/phase-helpers.sh
@@ -1062,9 +1062,42 @@ if ___eapi_has_eapply; then
 fi
 
 if ___eapi_has_eapply_user; then
+       # Considers the first operand as a directory pathname and attempts to
+       # read its immediate entries into an array variable named 'dirents'. If
+       # the operand is unspecified or empty, the current working directory
+       # shall be read. The array indices might not begin from 0, and might
+       # not be contiguous. If both the . and .. entries are seen, the return
+       # value shall be 0. Otherwise, it shall be greater than 0.
+       __readdir() {
+               local path=$1
+               local reset_shopts count i
+
+               # The globskipdots option was introduced by bash-5.2. Unless
+               # disabled, it prevents the matching of the . and .. entries.
+               reset_shopts=$(
+                       shopt -p globskipdots 2>/dev/null
+                       shopt -p nullglob extglob
+               )
+               [[ ${reset_shopts} == *globskipdots* ]] && shopt -u globskipdots
+               shopt -s nullglob extglob
+               [[ ${path} && ${path} != */ ]] && path+=/
+               eval 'dirents=( "${path}"@(.?(.)|*) );' "${reset_shopts}"
+
+               # For the . and .. entries to exist implies beyond a reasonable
+               # doubt that the path is a directory and was successfully read.
+               for i in "${!dirents[@]}"; do
+                       if [[ ${dirents[i]##*/} == .?(.) ]]; then
+                               unset -v 'dirents[i]'
+                               (( ++count == 2 )) && return
+                       fi
+               done
+               return 1
+       }
+
        eapply_user() {
                local basename basedir columns tagfile hr d f
                local -A patch_by
+               local -a dirents
 
                [[ ${EBUILD_PHASE} == prepare ]] || \
                        die "eapply_user() called during invalid phase: 
${EBUILD_PHASE}"
@@ -1094,7 +1127,11 @@ if ___eapi_has_eapply_user; then
                # 3. ${CATEGORY}/${PN}
                # all of the above may be optionally followed by a slot
                for d in 
"${basedir}"/"${CATEGORY}"/{"${PN}","${P}","${P}-${PR}"}{,":${SLOT%/*}"}; do
-                       for f in "${d}"/*; do
+                       if ! __readdir "${d}" && [[ -e ${d} || -L ${d} ]]; then
+                               __helpers_die "eapply_user: ${d@Q} exists but 
can't be opened as a directory by ${PORTAGE_BUILD_USER@Q}"
+                               return
+                       fi
+                       for f in "${dirents[@]}"; do
                                if [[ ${f} == *.@(diff|patch) ]]; then
                                        basename=${f##*/}
                                        if [[ -s ${f} ]]; then

Reply via email to