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
