Blob Blame History Raw
#!/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+"$@"}