forked from mirrors/nixpkgs
4765a3e153
Fully enabling crossSystem support for autoPatchelfHook came with some perhaps unintended consequences of being a bit more aggressive about patching ELF files from architectures/ABIs that differ from the target (previously, those files would be ignored because ldd usually couldn't handle them). This change adds architecture and rough OS ABI detection to the script so that it doesn't try to blindly replace the interpreter of files that can't possibly use that interpreter, and also makes sure it doesn't accidentally use libraries of other architectures/ABIs.
382 lines
13 KiB
Bash
382 lines
13 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
declare -a autoPatchelfLibs
|
|
declare -Ag autoPatchelfFailedDeps
|
|
|
|
gatherLibraries() {
|
|
autoPatchelfLibs+=("$1/lib")
|
|
}
|
|
|
|
# wrapper around patchelf to raise proper error messages
|
|
# containing the tried file name and command
|
|
runPatchelf() {
|
|
patchelf "$@" || (echo "Command failed: patchelf $*" && exit 1)
|
|
}
|
|
|
|
# shellcheck disable=SC2154
|
|
# (targetOffset is referenced but not assigned.)
|
|
addEnvHooks "$targetOffset" gatherLibraries
|
|
|
|
isExecutable() {
|
|
# For dynamically linked ELF files it would be enough to check just for the
|
|
# INTERP section. However, we won't catch statically linked executables as
|
|
# they only have an ELF type of EXEC but no INTERP.
|
|
#
|
|
# So what we do here is just check whether *either* the ELF type is EXEC
|
|
# *or* there is an INTERP section. This also catches position-independent
|
|
# executables, as they typically have an INTERP section but their ELF type
|
|
# is DYN.
|
|
isExeResult="$(LANG=C $READELF -h -l "$1" 2> /dev/null \
|
|
| grep '^ *Type: *EXEC\>\|^ *INTERP\>')"
|
|
# not using grep -q, because it can cause Broken pipe
|
|
# https://unix.stackexchange.com/questions/305547/broken-pipe-when-grepping-output-but-only-with-i-flag
|
|
[ -n "$isExeResult" ]
|
|
}
|
|
|
|
# We cache dependencies so that we don't need to search through all of them on
|
|
# every consecutive call to findDependency.
|
|
declare -Ag autoPatchelfCachedDepsAssoc
|
|
declare -ag autoPatchelfCachedDeps
|
|
|
|
addToDepCache() {
|
|
if [[ ${autoPatchelfCachedDepsAssoc[$1]+f} ]]; then return; fi
|
|
|
|
# store deps in an assoc. array for efficient lookups
|
|
# otherwise findDependency would have quadratic complexity
|
|
autoPatchelfCachedDepsAssoc["$1"]=""
|
|
|
|
# also store deps in normal array to maintain their order
|
|
autoPatchelfCachedDeps+=("$1")
|
|
}
|
|
|
|
declare -gi depCacheInitialised=0
|
|
declare -gi doneRecursiveSearch=0
|
|
declare -g foundDependency
|
|
|
|
getDepsFromElfBinary() {
|
|
# NOTE: This does not use runPatchelf because it may encounter non-ELF
|
|
# files. Caller is expected to check the return code if needed.
|
|
patchelf --print-needed "$1" 2> /dev/null
|
|
}
|
|
|
|
getRpathFromElfBinary() {
|
|
# NOTE: This does not use runPatchelf because it may encounter non-ELF
|
|
# files. Caller is expected to check the return code if needed.
|
|
local rpath
|
|
IFS=':' read -ra rpath < <(patchelf --print-rpath "$1" 2> /dev/null) || return $?
|
|
|
|
printf "%s\n" "${rpath[@]}"
|
|
}
|
|
|
|
populateCacheForDep() {
|
|
local so="$1"
|
|
local rpath found
|
|
rpath="$(getRpathFromElfBinary "$so")" || return 1
|
|
|
|
for found in $(getDepsFromElfBinary "$so"); do
|
|
local rpathElem
|
|
for rpathElem in $rpath; do
|
|
# Ignore empty element or $ORIGIN magic variable which should be
|
|
# deterministically resolved by adding this package's library
|
|
# files early anyway.
|
|
#
|
|
# shellcheck disable=SC2016
|
|
# (Expressions don't expand in single quotes, use double quotes for
|
|
# that.)
|
|
if [[ -z "$rpathElem" || "$rpathElem" == *'$ORIGIN'* ]]; then
|
|
continue
|
|
fi
|
|
|
|
local soname="${found%.so*}"
|
|
local foundso=
|
|
for foundso in "$rpathElem/$soname".so*; do
|
|
addToDepCache "$foundso"
|
|
done
|
|
|
|
# Found in this element of the rpath, no need to check others.
|
|
if [ -n "$foundso" ]; then
|
|
break
|
|
fi
|
|
done
|
|
done
|
|
|
|
# Not found in any rpath element.
|
|
return 1
|
|
}
|
|
|
|
populateCacheWithRecursiveDeps() {
|
|
# Dependencies may add more to the end of this array, so we use a counter
|
|
# with while instead of a regular for loop here.
|
|
local -i i=0
|
|
while [ $i -lt ${#autoPatchelfCachedDeps[@]} ]; do
|
|
populateCacheForDep "${autoPatchelfCachedDeps[$i]}"
|
|
i=$i+1
|
|
done
|
|
}
|
|
|
|
getBinArch() {
|
|
$OBJDUMP -f "$1" 2> /dev/null | sed -ne 's/^architecture: *\([^,]\+\).*/\1/p'
|
|
}
|
|
|
|
# Returns the specific OS ABI for an ELF file in the format produced by
|
|
# readelf(1), like "UNIX - System V" or "UNIX - GNU".
|
|
getBinOsabi() {
|
|
$READELF -h "$1" 2> /dev/null | sed -ne 's/^[ \t]*OS\/ABI:[ \t]*\(.*\)/\1/p'
|
|
}
|
|
|
|
# Tests whether two OS ABIs are compatible, taking into account the generally
|
|
# accepted compatibility of SVR4 ABI with other ABIs.
|
|
areBinOsabisCompatible() {
|
|
local wanted="$1"
|
|
local got="$2"
|
|
|
|
if [[ -z "$wanted" || -z "$got" ]]; then
|
|
# One of the types couldn't be detected, so as a fallback we'll assume
|
|
# they're compatible.
|
|
return 0
|
|
fi
|
|
|
|
# Generally speaking, the base ABI (0x00), which is represented by
|
|
# readelf(1) as "UNIX - System V", indicates broad compatibility with other
|
|
# ABIs.
|
|
#
|
|
# TODO: This isn't always true. For example, some OSes embed ABI
|
|
# compatibility into SHT_NOTE sections like .note.tag and .note.ABI-tag.
|
|
# It would be prudent to add these to the detection logic to produce better
|
|
# ABI information.
|
|
if [[ "$wanted" == "UNIX - System V" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Similarly here, we should be able to link against a superset of features,
|
|
# so even if the target has another ABI, this should be fine.
|
|
if [[ "$got" == "UNIX - System V" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Otherwise, we simply return whether the ABIs are identical.
|
|
if [[ "$wanted" == "$got" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# NOTE: If you want to use this function outside of the autoPatchelf function,
|
|
# keep in mind that the dependency cache is only valid inside the subshell
|
|
# spawned by the autoPatchelf function, so invoking this directly will possibly
|
|
# rebuild the dependency cache. See the autoPatchelf function below for more
|
|
# information.
|
|
findDependency() {
|
|
local filename="$1"
|
|
local arch="$2"
|
|
local osabi="$3"
|
|
local lib dep
|
|
|
|
if [ $depCacheInitialised -eq 0 ]; then
|
|
for lib in "${autoPatchelfLibs[@]}"; do
|
|
for so in "$lib/"*.so*; do addToDepCache "$so"; done
|
|
done
|
|
depCacheInitialised=1
|
|
fi
|
|
|
|
for dep in "${autoPatchelfCachedDeps[@]}"; do
|
|
if [ "$filename" = "${dep##*/}" ]; then
|
|
if [ "$(getBinArch "$dep")" = "$arch" ] && areBinOsabisCompatible "$osabi" "$(getBinOsabi "$dep")"; then
|
|
foundDependency="$dep"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Populate the dependency cache with recursive dependencies *only* if we
|
|
# didn't find the right dependency so far and afterwards run findDependency
|
|
# again, but this time with $doneRecursiveSearch set to 1 so that it won't
|
|
# recurse again (and thus infinitely).
|
|
if [ $doneRecursiveSearch -eq 0 ]; then
|
|
populateCacheWithRecursiveDeps
|
|
doneRecursiveSearch=1
|
|
findDependency "$filename" "$arch" || return 1
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
autoPatchelfFile() {
|
|
local dep rpath="" toPatch="$1"
|
|
|
|
local interpreter
|
|
interpreter="$(< "$NIX_CC/nix-support/dynamic-linker")"
|
|
|
|
local interpreterArch interpreterOsabi toPatchArch toPatchOsabi
|
|
interpreterArch="$(getBinArch "$interpreter")"
|
|
interpreterOsabi="$(getBinOsabi "$interpreter")"
|
|
toPatchArch="$(getBinArch "$toPatch")"
|
|
toPatchOsabi="$(getBinOsabi "$toPatch")"
|
|
|
|
if [ "$interpreterArch" != "$toPatchArch" ]; then
|
|
# Our target architecture is different than this file's architecture,
|
|
# so skip it.
|
|
echo "skipping $toPatch because its architecture ($toPatchArch) differs from target ($interpreterArch)" >&2
|
|
return 0
|
|
elif ! areBinOsabisCompatible "$interpreterOsabi" "$toPatchOsabi"; then
|
|
echo "skipping $toPatch because its OS ABI ($toPatchOsabi) is not compatible with target ($interpreterOsabi)" >&2
|
|
return 0
|
|
fi
|
|
|
|
if isExecutable "$toPatch"; then
|
|
runPatchelf --set-interpreter "$interpreter" "$toPatch"
|
|
# shellcheck disable=SC2154
|
|
# (runtimeDependencies is referenced but not assigned.)
|
|
if [ -n "$runtimeDependencies" ]; then
|
|
for dep in $runtimeDependencies; do
|
|
rpath="$rpath${rpath:+:}$dep/lib"
|
|
done
|
|
fi
|
|
fi
|
|
|
|
local libcLib
|
|
libcLib="$(< "$NIX_CC/nix-support/orig-libc")/lib"
|
|
|
|
echo "searching for dependencies of $toPatch" >&2
|
|
|
|
local missing
|
|
missing="$(getDepsFromElfBinary "$toPatch")" || return 0
|
|
|
|
# This ensures that we get the output of all missing dependencies instead
|
|
# of failing at the first one, because it's more useful when working on a
|
|
# new package where you don't yet know its dependencies.
|
|
|
|
for dep in $missing; do
|
|
if [[ "$dep" == /* ]]; then
|
|
# This is an absolute path. If it exists, just use it. Otherwise,
|
|
# we probably want this to produce an error when checked (because
|
|
# just updating the rpath won't satisfy it).
|
|
if [ -f "$dep" ]; then
|
|
continue
|
|
fi
|
|
elif [ -f "$libcLib/$dep" ]; then
|
|
# This library exists in libc, and will be correctly resolved by
|
|
# the linker.
|
|
continue
|
|
fi
|
|
|
|
echo -n " $dep -> " >&2
|
|
if findDependency "$dep" "$toPatchArch" "$toPatchOsabi"; then
|
|
rpath="$rpath${rpath:+:}${foundDependency%/*}"
|
|
echo "found: $foundDependency" >&2
|
|
else
|
|
echo "not found!" >&2
|
|
autoPatchelfFailedDeps["$dep"]="$toPatch"
|
|
fi
|
|
done
|
|
|
|
if [ -n "$rpath" ]; then
|
|
echo "setting RPATH to: $rpath" >&2
|
|
runPatchelf --set-rpath "$rpath" "$toPatch"
|
|
fi
|
|
}
|
|
|
|
# Can be used to manually add additional directories with shared object files
|
|
# to be included for the next autoPatchelf invocation.
|
|
addAutoPatchelfSearchPath() {
|
|
local -a findOpts=()
|
|
|
|
# XXX: Somewhat similar to the one in the autoPatchelf function, maybe make
|
|
# it DRY someday...
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--) shift; break;;
|
|
--no-recurse) shift; findOpts+=("-maxdepth" 1);;
|
|
--*)
|
|
echo "addAutoPatchelfSearchPath: ERROR: Invalid command line" \
|
|
"argument: $1" >&2
|
|
return 1;;
|
|
*) break;;
|
|
esac
|
|
done
|
|
|
|
while IFS= read -r -d '' file; do
|
|
addToDepCache "$file"
|
|
done < <(find "$@" "${findOpts[@]}" \! -type d \
|
|
\( -name '*.so' -o -name '*.so.*' \) -print0)
|
|
}
|
|
|
|
autoPatchelf() {
|
|
local norecurse=
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--) shift; break;;
|
|
--no-recurse) shift; norecurse=1;;
|
|
--*)
|
|
echo "autoPatchelf: ERROR: Invalid command line" \
|
|
"argument: $1" >&2
|
|
return 1;;
|
|
*) break;;
|
|
esac
|
|
done
|
|
|
|
if [ $# -eq 0 ]; then
|
|
echo "autoPatchelf: No paths to patch specified." >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "automatically fixing dependencies for ELF files" >&2
|
|
|
|
# Add all shared objects of the current output path to the start of
|
|
# autoPatchelfCachedDeps so that it's chosen first in findDependency.
|
|
addAutoPatchelfSearchPath ${norecurse:+--no-recurse} -- "$@"
|
|
|
|
while IFS= read -r -d $'\0' file; do
|
|
isELF "$file" || continue
|
|
segmentHeaders="$(LANG=C $READELF -l "$file")"
|
|
# Skip if the ELF file doesn't have segment headers (eg. object files).
|
|
# not using grep -q, because it can cause Broken pipe
|
|
grep -q '^Program Headers:' <<<"$segmentHeaders" || continue
|
|
if isExecutable "$file"; then
|
|
# Skip if the executable is statically linked.
|
|
grep -q "^ *INTERP\\>" <<<"$segmentHeaders" || continue
|
|
fi
|
|
# Jump file if patchelf is unable to parse it
|
|
# Some programs contain binary blobs for testing,
|
|
# which are identified as ELF but fail to be parsed by patchelf
|
|
patchelf "$file" || continue
|
|
autoPatchelfFile "$file"
|
|
done < <(find "$@" ${norecurse:+-maxdepth 1} -type f -print0)
|
|
|
|
# fail if any dependencies were not found and
|
|
# autoPatchelfIgnoreMissingDeps is not set
|
|
local depsMissing=0
|
|
for failedDep in "${!autoPatchelfFailedDeps[@]}"; do
|
|
echo "autoPatchelfHook could not satisfy dependency $failedDep wanted by ${autoPatchelfFailedDeps[$failedDep]}"
|
|
depsMissing=1
|
|
done
|
|
# shellcheck disable=SC2154
|
|
# (autoPatchelfIgnoreMissingDeps is referenced but not assigned.)
|
|
if [[ $depsMissing == 1 && -z "$autoPatchelfIgnoreMissingDeps" ]]; then
|
|
echo "Add the missing dependencies to the build inputs or set autoPatchelfIgnoreMissingDeps=true"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# XXX: This should ultimately use fixupOutputHooks but we currently don't have
|
|
# a way to enforce the order. If we have $runtimeDependencies set, the setup
|
|
# hook of patchelf is going to ruin everything and strip out those additional
|
|
# RPATHs.
|
|
#
|
|
# So what we do here is basically run in postFixup and emulate the same
|
|
# behaviour as fixupOutputHooks because the setup hook for patchelf is run in
|
|
# fixupOutput and the postFixup hook runs later.
|
|
#
|
|
# shellcheck disable=SC2016
|
|
# (Expressions don't expand in single quotes, use double quotes for that.)
|
|
postFixupHooks+=('
|
|
if [ -z "${dontAutoPatchelf-}" ]; then
|
|
autoPatchelf -- $(for output in $outputs; do
|
|
[ -e "${!output}" ] || continue
|
|
echo "${!output}"
|
|
done)
|
|
fi
|
|
')
|