#!/bin/bash
#
# livepatch build script
#
# Copyright (C) 2014 Seth Jennings <sjenning@redhat.com>
# Copyright (C) 2013,2014 Josh Poimboeuf <jpoimboe@redhat.com>
# Copyright (C) 2015 Ross Lagerwall <ross.lagerwall@citrix.com>
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This script takes a Xen tree, and a patch and outputs an livepatch
# module intended to patch Xen at runtime.
# Large amounts of this script are taken from kpatch's kpatch-build
# script.

SCRIPTDIR="$(readlink -f $(dirname $(type -p $0)))"
WORKDIR="$(readlink -f -- .)"
CPUS="$(getconf _NPROCESSORS_ONLN)"
DEBUG=n
XEN_DEBUG=n
SKIP=
DEPENDS=
XEN_DEPENDS=
PRELINK=
STRIP=0
XENSYMS=xen-syms

warn() {
    echo "ERROR: $1" >&2
}

die() {
    if [[ -z $1 ]]; then
        msg="LivePatch build failed"
    else
        msg="$1"
    fi

    warn "$msg."

    exit 1
}

find_tools() {
    if [[ -e "$SCRIPTDIR/create-diff-object" ]]; then
        # Running from source tree
        TOOLSDIR="$SCRIPTDIR"
    elif [[ -e "$SCRIPTDIR/../libexec/livepatch-build-tools/create-diff-object" ]]; then
        # Running installed
        TOOLSDIR="$(readlink -f $SCRIPTDIR/../libexec/livepatch-build-tools)"
    else
        return 1
    fi
}

function make_patch_name()
{
    PATCHNAME=$(basename "$1")
    if [[ "$PATCHNAME" =~ \.patch ]] || [[ "$PATCHNAME" =~ \.diff ]]; then
            PATCHNAME="${PATCHNAME%.*}"
    fi

    # Only allow alphanumerics and '_' and '-' in the patch name.  Everything
    # else is replaced with '-'.  Truncate to 127 chars
    # (XEN_LIVEPATCH_NAME_SIZE - 1).
    echo ${PATCHNAME//[^a-zA-Z0-9_-]/-} |cut -c -127
}

# Do a full normal build
function build_full()
{
    cd "${SRCDIR}/xen" || die
    make "-j$CPUS" clean &> "${OUTPUT}/build_full_clean.log" || die
    make "-j$CPUS" $XEN_DEBUG &> "${OUTPUT}/build_full_compile.log" || die
    cp xen-syms "$OUTPUT"
}

# Build with special GCC flags
function build_special()
{
    name=$1

    cd "${SRCDIR}" || die

    # Capture .o files from the patched build
    export LIVEPATCH_BUILD_DIR="$(pwd)/"
    export LIVEPATCH_CAPTURE_DIR="$OUTPUT/${name}"
    mkdir -p "$LIVEPATCH_CAPTURE_DIR"

    # Build with special GCC flags
    cd "${SRCDIR}/xen" || die
    if grep -q 'nostdinc' Rules.mk; then
        # Support for old build system, attempt to set -f{function,data}-sections and rebuild
        sed -i 's/CFLAGS += -nostdinc/CFLAGS += -nostdinc -ffunction-sections -fdata-sections/' Rules.mk
        cp -p arch/x86/Makefile arch/x86/Makefile.bak
        sed -i 's/--section-alignment=0x200000/--section-alignment=0x1000/' arch/x86/Makefile
        # Restore timestamps to prevent spurious rebuilding
        touch --reference=arch/x86/Makefile.bak arch/x86/Makefile
        make "-j$CPUS" $XEN_DEBUG &> "${OUTPUT}/build_${name}_compile.log" || die
        sed -i 's/CFLAGS += -nostdinc -ffunction-sections -fdata-sections/CFLAGS += -nostdinc/' Rules.mk
        mv -f arch/x86/Makefile.bak arch/x86/Makefile
    else
        # -f{function,data}-sections set by CONFIG_LIVEPATCH
        make "-j$CPUS" $XEN_DEBUG &> "${OUTPUT}/build_${name}_compile.log" || die
    fi

    unset LIVEPATCH_BUILD_DIR
    unset LIVEPATCH_CAPTURE_DIR
}

elf_section_exists ()
{
    local ELF="$1"
    local SEC="$2"

    objdump -h -j "$SEC" "$ELF" &> /dev/null
}

# Extract a set of unique symbols for a specified section.
elf_extract_section_symbols ()
{
    local -r ELF="$1"
    local -r SEC="$2"

    if elf_section_exists "$ELF" "$SEC"
    then
        # Example objdump command output to be parsed:
        #
        # SYMBOL TABLE:
        # 0000000000000000 l    d  .livepatch.funcs    0000000000000000 .livepatch.funcs
        objdump -w -j "$SEC" -t "$ELF" | awk '/^SYMBOL TABLE:/ {seen = 1; next} seen && $NF {print $NF}' | sort -u
    fi
}

# Strip all metadata symbols belonging to a metadata section
# or whose name starts with a livepatch hook prefix.
# The function constructs the 'strip' utility command line
# and then invokes strip with that command line.
strip_metadata_symbols ()
{
    local -r FILE="$1"
    local -a STRIP_CMD_OPTS=()
    local -a SYM_SECTIONS=(".livepatch.funcs")
    local -a SYM_PREFIX=("livepatch_load_data_"
                         "livepatch_unload_data_"
                         "livepatch_preapply_data_"
                         "livepatch_apply_data_"
                         "livepatch_postapply_data_"
                         "livepatch_prerevert_data_"
                         "livepatch_revert_data_"
                         "livepatch_postrevert_data_")
    local -a SYMS=()

    # Enable wildcard
    STRIP_CMD_OPTS+=("-w")

    # Strip all livepatch hooks metadata symbols
    for sym in "${SYM_PREFIX[@]}"; do
        STRIP_CMD_OPTS+=("-N")
        STRIP_CMD_OPTS+=("\"${sym}*\"")
    done

    # Find all symbols from metadata sections
    # Note: There may be name conflicts between global
    # and local symbols belonging to the same section.
    # For the '.livepatch.funcs' section it is not a
    # problem. Think about it before adding more sections.
    for sec in "${SYM_SECTIONS[@]}"; do
        SYMS+=($(elf_extract_section_symbols "$FILE" "$sec"))
    done

    # Strip metadata sections' symbols
    if [ ${#SYMS[@]} -gt 0 ]
    then
        for sym in "${SYMS[@]}"; do
            STRIP_CMD_OPTS+=("-N")
            STRIP_CMD_OPTS+=("${sym}")
        done
    fi

    strip "${STRIP_CMD_OPTS[@]}" "$FILE"
}

function gnu_section()
{
    perl -e '$d = pack("H*", $ARGV[0]); print pack("VVVZ*", 4, length($d), 3, "GNU").$d' "$1"
}

function create_patch()
{
    echo "Extracting new and modified ELF sections..."

    [[ -e "${OUTPUT}/original/changed_objs" ]] || die "no changed objects found"
    [[ -e "${OUTPUT}/patched/changed_objs" ]] || die "no changed objects found"

    cd "${OUTPUT}/original" || die
    FILES="$(find xen -type f -name "*.o")"
    cd "${OUTPUT}" || die
    CHANGED=0
    ERROR=0
    debugopt=
    [[ $DEBUG -eq 1 ]] && debugopt=-d

    for i in $FILES; do
        mkdir -p "output/$(dirname $i)" || die
        echo "Processing ${i}"
        echo "Run create-diff-object on $i" >> "${OUTPUT}/create-diff-object.log"
        "${TOOLSDIR}"/create-diff-object $debugopt $PRELINK "original/$i" "patched/$i" "${OUTPUT}/xen-syms" "output/$i" &>> "${OUTPUT}/create-diff-object.log"
        rc="${PIPESTATUS[0]}"
        if [[ $rc = 139 ]]; then
            warn "create-diff-object SIGSEGV"
            if ls core* &> /dev/null; then
                cp core* /tmp
                die "core file at /tmp/$(ls core*)"
            fi
            die "no core file found, run 'ulimit -c unlimited' and try to recreate"
        fi
        # create-diff-object returns 3 if no functional change is found
        [[ $rc -eq 0 ]] || [[ $rc -eq 3 ]] || { ERROR=$(expr $ERROR "+" 1); warn "create-diff-object $i rc $rc"; }
        if [[ $rc -eq 0 ]]; then
            CHANGED=1
        fi
    done

    NEW_FILES=$(comm -23 <(cd patched && find xen/ -type f -name '*.o' | sort) <(cd original && find xen/ -type f -name '*.o' | sort))
    for i in $NEW_FILES; do
        mkdir -p "output/$(dirname "$i")"
        cp "patched/$i" "output/$i"
        [[ $STRIP -eq 1 ]] && strip --strip-unneeded "output/$i"
        CHANGED=1
    done

    if [[ $ERROR -ne 0 ]]; then
        die "$ERROR error(s) encountered"
    fi

    if [[ $CHANGED -eq 0 ]]; then
        die "no functional changes found"
    fi

    # Create a dependency section
    gnu_section "$DEPENDS" > depends.bin

    # Create a Xen dependency section
    gnu_section "$XEN_DEPENDS" > xen_depends.bin

    echo "Creating patch module..."
    if [ -z "$PRELINK" ]; then
        ld -r -o "${PATCHNAME}.livepatch" --build-id=sha1 $(find output -type f -name "*.o") || die
        chmod +x "${PATCHNAME}.livepatch"
    else
        ld -r -o output.o --build-id=sha1 $(find output -type f -name "*.o") || die
        "${TOOLSDIR}"/prelink $debugopt output.o "${PATCHNAME}.livepatch" "$XENSYMS" &>> "${OUTPUT}/prelink.log" || die
    fi

    strip_metadata_symbols "${PATCHNAME}.livepatch"

    objcopy --add-section .livepatch.depends=depends.bin "${PATCHNAME}.livepatch"
    objcopy --set-section-flags .livepatch.depends=alloc,readonly "${PATCHNAME}.livepatch"

    objcopy --add-section .livepatch.xen_depends=xen_depends.bin "${PATCHNAME}.livepatch"
    objcopy --set-section-flags .livepatch.xen_depends=alloc,readonly "${PATCHNAME}.livepatch"
}

usage() {
    echo "usage: $(basename $0) [options]" >&2
    echo "        -h, --help         Show this help message" >&2
    echo "        -s, --srcdir       Xen source directory" >&2
    echo "        -p, --patch        Patch file" >&2
    echo "        -c, --config       .config file" >&2
    echo "        -o, --output       Output directory" >&2
    echo "        -j, --cpus         Number of CPUs to use" >&2
    echo "        -k, --skip         Skip build or diff phase" >&2
    echo "        -d, --debug        Enable debug logging" >&2
    echo "        --xen-debug        Build debug Xen (if your .config does not have the options)" >&2
    echo "        --xen-syms         Build against a xen-syms" >&2
    echo "        --depends          Required build-id" >&2
    echo "        --xen-depends      Required Xen build-id" >&2
    echo "        --prelink          Prelink" >&2
    echo "        --strip            Remove all symbols that are not needed for relocation processing." >&2
}

find_tools || die "can't find supporting tools"

options=$(getopt -o hs:p:c:o:j:k:d -l "help,srcdir:,patch:,config:,output:,cpus:,skip:,debug,xen-debug,xen-syms:,depends:,xen-depends:,prelink,strip" -- "$@") || die "getopt failed"

eval set -- "$options"

while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            usage
            exit 0
            ;;
        -j|--cpus)
            shift
            CPUS="$1"
            shift
            ;;
        -k|--skip)
            shift
            SKIP="$1"
            shift
            ;;
        -d|--debug)
            DEBUG=1
            shift
            ;;
        --xen-debug)
            XEN_DEBUG=y
            shift
            ;;
        -s|--srcdir)
            shift
            srcarg="$1"
            shift
            ;;
        -p|--patch)
            shift
            patcharg="$1"
            shift
            ;;
        -c|--config)
            shift
            configarg="$1"
            shift
            ;;
        -o|--output)
            shift
            outputarg="$1"
            shift
            ;;
        --xen-syms)
            shift
            [ -f "$1" ] || die "xen-syms file does not exist"
            XENSYMS="$(readlink -f -- "$1")"
            shift
            ;;
        --depends)
            shift
            DEPENDS="$1"
            shift
            ;;
        --xen-depends)
            shift
            XEN_DEPENDS="$1"
            shift
            ;;
        --prelink)
            PRELINK=--resolve
            shift
            ;;
        --strip)
            STRIP=1
            shift
            ;;
        --)
            shift
            break
            ;;
    esac
done

[ -z "$srcarg" ] && die "Xen directory not given"
[ -d "$srcarg" ] || die "Xen directory does not exist"
[ -z "$patcharg" ] && die "Patchfile not given"
[ -f "$patcharg" ] || die "Patchfile does not exist"
[ -z "$configarg" ] && die ".config not given"
[ -f "$configarg" ] || die ".config does not exist"
[ -z "$outputarg" ] && die "Output directory not given"
[ -z "$DEPENDS" ] && die "Build-id dependency not given"
[ -z "$XEN_DEPENDS" ] && die "Xen Build-id dependency not given"

SRCDIR="$(readlink -f -- "$srcarg")"
# We need an absolute path because we move around, but we need to
# retain the name of the symlink (= realpath -s)
PATCHFILE="$(readlink -f "$(dirname "$patcharg")")/$(basename "$patcharg")"
CONFIGFILE="$(readlink -f -- "$configarg")"

PATCHNAME=$(make_patch_name "${PATCHFILE}")

echo "Building LivePatch patch: ${PATCHNAME}"
echo
echo "Xen directory: ${SRCDIR}"
echo "Patch file: ${PATCHFILE}"
echo ".config file: ${CONFIGFILE}"
echo "Output directory: $outputarg"
echo "================================================"
echo

if [ "${SKIP}" != "build" ]; then
    # Make sure output directory doesn't exist, and create it.
    [ -e "$outputarg" ] && die "Output directory exists"
    mkdir -p "$outputarg" || die
    OUTPUT="$(readlink -f -- "$outputarg")"

    grep -q 'CONFIG_LIVEPATCH=y' "${CONFIGFILE}" || die "CONFIG_LIVEPATCH must be enabled"
    cd "$SRCDIR" || die
    patch -s -N -p1 -f --fuzz=0 --dry-run < "$PATCHFILE" || die "Source patch file failed to apply"

    cp -f "${CONFIGFILE}" "${OUTPUT}/.config"
    cp -f "${OUTPUT}/.config" "xen/.config"

    grep -q CONFIG_DEBUG "xen/.config"
    if [ $? -eq 0 ]; then
        if [ "$XEN_DEBUG" == "y" ]; then
            grep -q "CONFIG_DEBUG=y" "xen/.config" || die "CONFIG_DEBUG and --xen-debug mismatch"
        fi
        XEN_DEBUG=""
    else
        XEN_DEBUG="debug=$XEN_DEBUG"
    fi

    export CROSS_COMPILE="${TOOLSDIR}/livepatch-gcc "

    echo "Perform full initial build with ${CPUS} CPU(s)..."
    build_full

    echo "Reading special section data"
    # Using xen-syms built in the previous step by build_full().
    SPECIAL_VARS=$(readelf -wi "$OUTPUT/xen-syms" |
               awk '
               BEGIN { a = b = e = 0 }
               a == 0 && /DW_AT_name.* alt_instr/ {a = 1; next}
               b == 0 && /DW_AT_name.* bug_frame/ {b = 1; next}
               e == 0 && /DW_AT_name.* exception_table_entry/ {e = 1; next}
               # Use shell printf to (possibly) convert from hex to decimal
               a == 1 {printf("export ALT_STRUCT_SIZE=`printf \"%%d\" \"%s\"`\n", $4); a = 2}
               b == 1 {printf("export BUG_STRUCT_SIZE=`printf \"%%d\" \"%s\"`\n", $4); b = 2}
               e == 1 {printf("export EX_STRUCT_SIZE=`printf \"%%d\" \"%s\"`\n", $4); e = 2}
               a == 2 && b == 2 && e == 2 {exit}')
    [[ -n $SPECIAL_VARS ]] && eval "$SPECIAL_VARS"
    if [[ -z $ALT_STRUCT_SIZE ]] || [[ -z $BUG_STRUCT_SIZE ]] || [[ -z $EX_STRUCT_SIZE ]]; then
        die "can't find special struct size"
    fi
    for i in $ALT_STRUCT_SIZE $BUG_STRUCT_SIZE $EX_STRUCT_SIZE; do
        if [[ ! $i -gt 0 ]] || [[ ! $i -le 16 ]]; then
            die "invalid special struct size $i"
        fi
    done

    echo "Apply patch and build with ${CPUS} CPU(s)..."
    cd "$SRCDIR" || die
    patch -s -N -p1 -f --fuzz=0 < "$PATCHFILE" || die
    build_special patched

    echo "Unapply patch and build with ${CPUS} CPU(s)..."
    cd "$SRCDIR" || die
    patch -s -R -p1 -f --fuzz=0 < "$PATCHFILE" || die
    build_special original
fi

if [ "${SKIP}" != "diff" ]; then
    cd "${WORKDIR}" || die
    [ -d "$outputarg" ] || die "Output directory does not exist"
    OUTPUT="$(readlink -f -- "$outputarg")"

    cd "${OUTPUT}" || die
    create_patch
    echo "${PATCHNAME}.livepatch created successfully"
fi
