#!/bin/bash
# Script to check for ABI conflicts in annotated binaries.
#
# Created by Nick Clifton. <nickc@redhat.com>
# Copyright (c) 2017-2018 Red Hat.
#
# This is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 3, or (at your
# option) any later version.
# It is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Usage:
# check-abi [switches] file(s)
#
# This script does not handle directories. This is deliberate.
# It is intended that if recursion is needed then it will be
# invoked from find, like this:
#
# find . -type f -exec check-abi {} \;
version=3.3
help ()
{
# The following exec goop is so that we don't have to manually
# redirect every message to stderr in this function.
exec 4>&1 # save stdout fd to fd #4
exec 1>&2 # redirect stdout to stderr
cat <<__EOM__
This is a shell script to check that the given program(s) have not
been built with object files that contain conflicting ABI options.
Usage: $prog {files|options}
{options} are:
-h --help Display this information.
-v --version Report the version number of this script.
-s --silent Produce no output, just an exit status.
-q --quiet Do not include the script name in the output.
-V --verbose Report on progress.
-i --inconsistencies Only report potential ABI problems.
-r=<PATH> --readelf=<PATH> Path to version of readelf to use to read notes.
-t=<PATH> --tmpfile=<PATH> Temporary file to use.
--ignore-unknown Silently skip files of unknown type.
--ignore-ABI Do not check ABI annotation.
--no-ignore-ABI Check ABI information but do not complain if none is found.
--ignore-enum Do not check enum size annotation.
--no-ignore-enum Check enum size information but do not complain if none is found.
--ignore-FORTIFY Do not check FORTIFY SOURCE annotation.
--no-ignore-FORTIFY Check FORTIFY SOURCE information but do not complain if none is found.
--ignore-stack-prot Do not check stack protection annotation.
--no-ignore-stack-prot Check stack protection information but do not complain if none is found.
--ignore-gaps Do not fail if there are gaps in the coverage.
--no-ignore-gaps Fail if there are gaps in the coverage.
-- Stop accumulating options.
The exit code indicates the status:
0: No FAIl or MAYBE results found
1: The file could not be parsed or at least one FAIL result found
2: Some MAYBE results, but no FAIL results
3: Some other kind of failure
__EOM__
exec 1>&4 # Copy stdout fd back from temporary save fd, #4
}
main ()
{
init
parse_args ${1+"$@"}
scan_files
if [ $failed -ne 0 ];
then
exit 1
else
if [ $maybe -ne 0 ];
then
exit 2
else
exit 0
fi
fi
}
report ()
{
if [ $silent -ne 0 ];
then
return
fi
if [ $quiet -eq 0 ];
then
echo -n $prog": "
fi
echo ${1+"$@"}
}
fail ()
{
report "Internal error: " ${1+"$@"}
exit 3
}
verbose ()
{
if [ $verb -ne 0 ]
then
report ${1+"$@"}
fi
}
# Initialise global variables.
init ()
{
files[0]="";
# num_files is the number of files to be listed minus one.
# This is because we are indexing the files[] array from zero.
num_files=0;
failed=0
maybe=0
silent=0
verb=0
quiet=0
inconsistencies=0
ignore_abi=0
ignore_enum=0
ignore_fortify=0
ignore_stack_prot=0
ignore_unknown=0
ignore_gaps=0
scanner=readelf
tmpfile=/dev/shm/check.abi.delme
}
# Parse our command line
parse_args ()
{
prog=`basename $0`;
# Locate any additional command line switches
# Likewise accumulate non-switches to the files list.
while [ $# -gt 0 ]
do
optname="`echo $1 | sed 's,=.*,,'`"
optarg="`echo $1 | sed 's,^[^=]*=,,'`"
case "$optname" in
-v | --version)
report "version: $version"
exit 0
;;
-h | --help)
help
exit 0
;;
-s | --silent)
silent=1;
verb=0;
;;
-q | --quiet)
quiet=1;
;;
-V | --verbose)
silent=0;
verb=1;
;;
-i | --inconsistencies)
silent=0;
inconsistencies=1;
;;
-r | --readelf)
if test "x$optarg" = "x$optname" ;
then
shift
if [ $# -eq 0 ]
then
fail "-$optname option needs a program name"
else
scanner=$1
fi
else
scanner="$optarg"
fi
;;
-t | --tmpfile)
if test "x$optarg" = "x$optname" ;
then
shift
if [ $# -eq 0 ]
then
fail "-t option needs a file name"
else
tmpfile=$1
fi
else
tmpfile="$optarg"
fi
;;
--ignore-unknown)
ignore_unknown=1;
;;
--ignore-abi | --ignore-ABI)
ignore_abi=1;
;;
--no-ignore-abi | --no-ignore-ABI)
ignore_abi=2;
;;
--ignore-enum)
ignore_enum=1;
;;
--no-ignore-enum)
ignore_enum=2;
;;
--ignore-fortify | --ignore-FORTIFY)
ignore_fortify=1;
;;
--no-ignore-fortify | --no-ignore-FORTIFY)
ignore_fortify=2;
;;
--ignore-stack-prot)
ignore_stack_prot=1;
;;
--no-ignore-stack-prot)
ignore_stack_prot=2;
;;
--ignore-gaps)
ignore_gaps=1;
;;
--no-ignore-gaps)
ignore_gaps=0;
;;
--)
shift
break;
;;
--*)
report "unrecognised option: $1"
help
exit 1
;;
*)
files[$num_files]="$1";
let "num_files++"
;;
esac
shift
done
# Accumulate any remaining arguments without processing them.
while [ $# -gt 0 ]
do
files[$num_files]="$1";
let "num_files++";
shift
done
if [ $num_files -gt 0 ];
then
# Remember that we are counting from zero not one.
let "num_files--"
else
report "must specify at least one file to scan"
exit 1
fi
}
scan_files ()
{
local i
i=0;
while [ $i -le $num_files ]
do
scan_file i
let "i++"
done
}
scan_file ()
{
local file
# Paranoia checks - the user should never encounter these.
if test "x$1" = "x" ;
then
fail "scan_file called without an argument"
fi
if test "x$2" != "x" ;
then
fail "scan_file called with too many arguments"
fi
# Use quotes when accessing files in order to preserve
# any spaces that might be in the directory name.
file="${files[$1]}";
# Catch names that start with a dash - they might confuse readelf
if test "x${file:0:1}" = "x-" ;
then
file="./$file"
fi
if ! [ -a "$file" ]
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: file not found"
failed=1
fi
return
fi
if ! [ -f "$file" ]
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: not an ordinary file"
failed=1
fi
return
fi
if ! [ -r "$file" ]
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: not readable"
failed=1
fi
return
fi
file $file | grep --silent -e ELF
if [ $? != 0 ];
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: not an ELF format file"
failed=1
fi
return
fi
$scanner --wide --notes $file > $tmpfile 2>&1
if [ $? != 0 ];
then
report "$file: scanner '$scanner' failed - see $tmpfile"
failed=1
# Leave the tmpfile intact so that it can be examined by the user.
return
fi
grep -q -e "Unknown note" $tmpfile
if [ $? == 0 ];
then
# The fortify and stack protection checks need parsed notes.
if [[ $ignore_fortify -eq 0 || $ignore_stack_prot -eq 0 ]];
then
report "$file: scanner '$scanner' could not parse the notes - see $tmpfile"
failed=1
# Leave the tmpfile intact so that it can be examined by the user.
return
fi
fi
verbose "NOTES: `cat $tmpfile`"
if [ $ignore_gaps -eq 0 ];
then
grep -q -e "Gap in build notes" $tmpfile
if [ $? == 0 ];
then
report "$file: there are gaps in the build notes"
failed=1
fi
fi
local -a abis
if [ $ignore_abi -ne 1 ];
then
# Convert:
# *<ABI>0x145e82c442000192 0x00000000 NT_GNU_BUILD...
# or:
# GA*<ABI>0x145e82c442000192 0x00000000 NT_GNU_BUILD...
# into:
# abis[n]=145e82c442000192
eval 'abis=($(grep -e \<ABI\> $tmpfile | cut -d " " -f 3 | cut -d x -f 2 | sort -u))'
verbose "ABI Info: ${abis[*]}"
if [ ${#abis[*]} -lt 1 ];
then
if [[ $ignore_abi -eq 0 && $inconsistencies -eq 0 ]];
then
report "$file: MAYBE: does not have an ABI note"
maybe=1
fi
else
if [ ${#abis[*]} -gt 1 ];
then
local i mismatch=0
if [ $inconsistencies -eq 0 ];
then
report "$file: contains ${#abis[*]} ABI notes"
fi
i=1;
while [ $i -lt ${#abis[*]} ]
do
if test "${abis[i]}" != "${abis[i-1]}" ;
then
# FIXME: Add code to differentiate between functions which have changed ABI and files ?
report "$file: FAIL: differing ABI values detected: ${abis[i]} vs ${abis[i-1]}"
failed=1
mismatch=1
fi
let "i++"
done
if [ $mismatch -eq 0 ];
then
verbose "$file: ABI: ${abis[0]}"
fi
fi
fi
fi
if [ $ignore_enum -ne 1 ];
then
# Convert:
# +<short enum>true or GA+<short enum>true
# into:
# abis[n]=true
# and
# !<short enum>false or GA!<short enum>false
# into:
# abis[n]=false
eval 'abis=($(grep -e "short enum" $tmpfile | cut -f 2 -d ">" | cut -f 1 -d " " | sort -u))'
verbose "Enum Info: ${abis[*]}"
if [ ${#abis[*]} -lt 1 ];
then
if [[ $ignore_enum -eq 0 && $inconsistencies -eq 0 ]];
then
report "$file: MAYBE: does not record enum size"
maybe=1
fi
else
if [ ${#abis[*]} -gt 1 ];
then
local i mismatch=0
if [ $inconsistencies -eq 0 ];
then
report "$file: contains ${#abis[*]} enum size notes"
fi
i=1;
while [ $i -lt ${#abis[*]} ]
do
if test "${abis[i]}" != "${abis[i-1]}" ;
then
report "$file: FAIL: differing -fshort-enums detected: ${abis[i]} vs ${abis[i-1]}"
failed=1
mismatch=1
fi
let "i++"
done
if [ $mismatch -eq 0 ];
then
verbose "$file: -fshort-enums: ${abis[0]}"
fi
fi
fi
fi
if [ $ignore_fortify -ne 1 ];
then
# Convert:
# *FORTIFY:0x1
# or:
# GA*FORTIFY:0x1
# into:
# abis[n]=1
eval 'abis=($(grep -e FORTIFY $tmpfile | cut -f 2 -d ":" | cut -b 3-5 | sed -e "s/ff/unknown/" | sort -u))'
verbose "Fortify Info: ${abis[*]}"
if [ ${#abis[*]} -lt 1 ];
then
if [[ $ignore_fortify -eq 0 && $inconsistencies -eq 0 ]];
then
report "$file: MAYBE: does not record _FORTIFY_SOURCE level"
maybe=1
fi
else
if [ ${#abis[*]} -gt 1 ];
then
local i mismatch=0
if [ $inconsistencies -eq 0 ];
then
report "$file: contains ${#abis[*]} FORTIFY_SOURCE notes"
fi
i=1;
while [ $i -lt ${#abis[*]} ]
do
if test "${abis[i]}" != "${abis[i-1]}" ;
then
report "$file: FAIL: differing FORTIFY SOURCE levels: ${abis[i]} vs ${abis[i-1]}"
failed=1;
mismatch=1;
fi
let "i++"
done
if [ $mismatch -eq 0 ];
then
verbose "$file: -D_FORTIFY_SOURCE=${abis[0]}"
fi
fi
fi
fi
if [ $ignore_stack_prot -ne 1 ];
then
# Convert:
# *<stack prot><type>
# into:
# abis[n]=<type>
eval 'abis=($(grep -e "stack prot" $tmpfile | cut -f 4 -d " " | cut -b 6- | sort -u))'
verbose "Stack Protection Info: ${abis[*]}"
if [ ${#abis[*]} -lt 1 ];
then
if [[ $ignore_stack_prot -eq 0 && $inconsistencies -eq 0 ]];
then
report "$file: MAYBE: does not record -fstack-protect status"
maybe=1
fi
else
if [ ${#abis[*]} -eq 1 ];
then
verbose "$file: -fstack-protect=${abis[0]}"
else
local i mismatch=0
if [ $inconsistencies -eq 0 ];
then
report "$file: contains ${#abis[*]} stack protection notes"
fi
i=1;
while [ $i -lt ${#abis[*]} ]
do
if test "${abis[i]}" != "${abis[i-1]}" ;
then
report "$file: FAIL: differing stack protection levels: ${abis[i]} vs ${abis[i-1]}"
failed=1;
mismatch=1;
fi
let "i++"
done
if [ $mismatch -eq 0 ];
then
verbose "$file: -fstack-protect=${abis[0]}"
fi
fi
fi
fi
rm -f $tmpfile
}
# Invoke main
main ${1+"$@"}