Blob Blame History Raw
#!/bin/bash
#
# weak-modules - determine which modules are kABI compatible with installed
#                kernels and set up the symlinks in /lib/*/weak-updates.
#

unset LANG LC_ALL LC_COLLATE

tmpdir=$(mktemp -td ${0##*/}.XXXXXX)
trap "rm -rf $tmpdir" EXIT
unset ${!changed_modules_*} ${!changed_initrd_*}

#!/bin/sh

# rpmsort: The sort in coreutils can't sort the RPM list how we want it so we
# instead transform the list into a form it will sort correctly, then sort.
rpmsort() {

	rpmlist=($(cat))

	if [ "$1" == "-r" ]; then
		reverse=1
	else
		reverse=0
	fi

        if [ ${#rpmlist[@]} = 1 ]; then
        # By definition already sorted
                return
        fi

        # Hash every value in the rpmlist
        for ((i = 0; i < ${#rpmlist[@]}; i++)); do
                rpmhash[i]="$(echo ${rpmlist[i]} | awk '{decode="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."; split($1,tmp,"");for (i=1;i<=length($1);i++){ printf "%02x",index(decode,tmp[i]) } }')"
        done

        for ((i = 0; i < ${#rpmlist[@]}; i++)); do

                for ((j = 0; j < ${#rpmlist[@]}-1; j++)); do

                        rpm1=${rpmlist[j]}      rpm2=${rpmlist[j+1]}
                        hex1=${rpmhash[j]}      hex2=${rpmhash[j+1]}

                        if [ ${#rpm1} -lt ${#rpm2} ]; then
                                len=$((${#rpm1}*2));
                        else
                                len=$((${#rpm2}*2));
                        fi

                        if [ "$(printf "%s\n" ${hex1:0:$len} ${hex2:0:$len} \
                              | sort | head -n 1)" = "$hex2" ]; then
                                rpmlist[j]=$rpm2; rpmlist[j+1]=$rpm1
                                rpmhash[j]=$hex2; rpmhash[j+1]=$hex1
                        fi
                done
        done

	if [ $reverse = 0 ]; then
        	for ((n = 0; n < ${#rpmlist[@]} ; n++)); do
                	echo "${rpmlist[$n]}"
        	done
	else
		for ((n = ${#rpmlist[@]}-1; n >= 0; n--)); do
			echo "${rpmlist[$n]}"
		done
	fi
}

# read_modules_list:
# Read in a list of modules from standard input. Convert the filenames into
# absolute paths and compute the kernel release for each module (either using
# the modinfo section or through the absolute path.
read_modules_list() {
    local IFS=$'\n'
    modules=($(cat))

    for ((n = 0; n < ${#modules[@]}; n++)); do
        if [ ${modules[n]:0:1} != '/' ]; then
            modules[n]="$PWD/${modules[n]}"
        fi
        if [ -f "${modules[n]}" ]; then
            module_krels[n]=$(krel_of_module ${modules[n]})
        else
            # Try to extract the kernel release from the path
            set -- "${modules[n]#/lib/modules/}"
            module_krels[n]=${1%%/*}
        fi
    done
}

# krel_of_module:
# Compute the kernel release of a module.
krel_of_module() {
    declare module=$1
    /sbin/modinfo -F vermagic "$module" | awk '{print $1}'
}

# module_is_compatible:
# Determine if a module is compatible with a particular kernel release. Also
# include any symbol deps that might be introduced by other external KMPs.
module_is_compatible() {
    declare module=$1 krel=$2 module_krel=$(krel_of_module "$module")

    if [ ! -e "$tmpdir/all-symvers-$krel-$module_krel" ]; then
        # Symbols exported by the "new" kernel
        if [ ! -e $tmpdir/symvers-$krel ]; then
            if [ -e /boot/symvers-$krel.gz ]; then
                zcat /boot/symvers-$krel.gz \
                | sed -r -ne 's:^(0x[0-9a-f]+\t[0-9a-zA-Z_]+)\t.*:\1:p'
            fi > $tmpdir/symvers-$krel
        fi

        # Symbols that other add-on modules of the "old" kernel export
        # (and that this module may require)
        if [ ! -e "$tmpdir/extra-symvers-$module_krel" ]; then
            if [ -e /lib/modules/$module_krel/extra ]; then
                find /lib/modules/$module_krel/extra -name '*.ko' \
                | xargs nm \
                | sed -nre 's:^([0-9a-f]+) A __crc_(.*):0x\1 \2:p'
            fi > $tmpdir/extra-symvers-$module_krel
        fi

        sort -u $tmpdir/symvers-$krel $tmpdir/extra-symvers-$module_krel \
        > "$tmpdir/all-symvers-$krel-$module_krel"
    fi

    # If the module does not have modversions enabled, $tmpdir/modvers
    # will be empty.
    /sbin/modprobe --dump-modversions "$module" \
    | sed -r -e 's:^(0x[0-9a-f]+\t.*):\1:' \
    | sort -u \
    > $tmpdir/modvers

    # Only include lines of the second file in the output that don't
    # match lines in the first file. (The default separator is
    # <space>, so we are matching the whole line.)
    join -j 1 -v 2 $tmpdir/all-symvers-$krel-$module_krel \
                   $tmpdir/modvers > $tmpdir/join

    if [ ! -s $tmpdir/modvers ]; then
        echo "Warning: Module ${module##*/} from kernel $module_krel has no" \
             "modversions, so it cannot be reused for kernel $krel" >&2
    elif [ -s $tmpdir/join ]; then
        [ -n "$verbose" ] &&
        echo "Module ${module##*/} from kernel $module_krel is not compatible" \             "with kernel $krel in symbols:" $(sed -e 's:.* ::' $tmpdir/join)
    else
        [ -n "$verbose" ] &&
        echo "Module ${module##*/} from kernel $module_krel is compatible" \
             "with kernel $krel"
        return 0
    fi
    return 1
}

# doit:
# A wrapper used whenever we're going to perform a real operation.
doit() {
    [ -n "$verbose" ] && echo "$@"
    [ -n "$dry_run" ] || "$@"
}

usage() {
    echo "Usage: ${0##*/} [options] {--add-modules|--remove-modules}"
    echo "${0##*/} [options] {--add-kernel|--remove-kernel} {kernel-release}"
    cat <<'EOF'
--add-modules
        Add a list of modules read from standard input. Create
        symlinks in compatible kernel's weak-updates/ directory.
        The list of modules is read from standard input.

--remove-modules
        Remove compatibility symlinks from weak-updates/ directories
        for a list of modules.  The list of modules is read from
        standard input.

--add-kernel
        Add compatibility symlinks for all compatible modules to the
        specified or running kernel.

--remove-kernel
        Remove all compatibility symlinks for the specified or current
        kernel.

--verbose
        Print the commands executed.

-dry-run
        Do not create/remove any files.
EOF
    exit $1
}

# module_has_changed:
# Mark if an actual change occured that we need to deal with later by calling
# depmod or mkinitrd against the affected kernel.
module_has_changed() {
    declare module=$1 krel=$2

    module=${module%.ko}
    module=${module##*/}

    eval "changed_modules_${krel//[^a-zA-Z0-9]/_}=$krel"
    eval "changed_initrd_${krel//[^a-zA-Z0-9]/_}=$krel"
}

# add_modules:
# Read in a list of modules from stdinput and process them for compatibility
# with installed kernels under /lib/modules.
add_modules() {
    read_modules_list || exit 1
    if [ ${#modules[@]} -gt 0 ]; then
        for krel in $(ls /lib/modules/); do
            [ -e "/boot/symvers-$krel.gz" ] || continue
            for ((n = 0; n < ${#modules[@]}; n++)); do
                module="${modules[n]}"
                module_krel="${module_krels[n]}"
                case "$module" in
                /lib/modules/$krel/*)
                    continue ;;
                esac
                subpath="${module#/lib/modules/$module_krel/extra}"
                weak_module="/lib/modules/$krel/weak-updates/${subpath#/}"
                if [ -r "$weak_module" ]; then
                    weak_krel=$(krel_of_module "$weak_module")
                    if [ "$weak_krel" != "$module_krel" ] &&
                       [ "$(printf "%s\n" "$weak_krel" "$module_krel" \
                            | rpmsort | head -n 1)" = \
                         "$module_krel" ]; then
                        # Keep modules from more recent kernels.
                        [ -n "$verbose" ] && echo \
"Keeping module ${module##*/} from kernel $weak_krel for kernel $krel"
                        continue
                    fi
                fi
                if module_is_compatible $module $krel; then
                    doit mkdir -p $(dirname $weak_module)
                    doit ln -sf $module $weak_module
                    module_has_changed $module $krel
                fi
            done
        done
    fi
}

# remove_modules:
# Read in a list of modules from stdinput and process them for removal.
remove_modules() {
    read_modules_list || exit 1
    if [ ${#modules[@]} -gt 0 ]; then
        krels=($(ls /lib/modules/ | rpmsort -r))
        for krel in "${krels[@]}"; do
            [ -e "/boot/symvers-$krel.gz" ] || continue
            for ((n = 0; n < ${#modules[@]}; n++)); do
                module="${modules[n]}"
                module_krel="${module_krels[n]}"
                subpath="${module#/lib/modules/$module_krel/extra}"
                weak_module="/lib/modules/$krel/weak-updates/${subpath#/}"
                if [ "$module" == "`readlink $weak_module`" ]; then
                    [ -n "$verbose" ] && echo \
"Removing compatible module ${module##*/} from kernel $krel"
                    doit rm -f "$weak_module"
                    for krel2 in "${krels[@]}"; do
                        [ -e "/boot/symvers-$krel2.gz" ] || continue
                        module="/lib/modules/$krel2/extra/$subpath"
                        [ -e "$module" ] || continue
                        if module_is_compatible "$module" "$krel2"; then
                            [ -n "$verbose" ] && echo \
"Adding compatible module ${module##*/} from kernel $krel2 instead"
                            doit ln -s "$module" "$weak_module"
                            break
                        fi
                    done
                    doit rmdir --parents --ignore-fail-on-non-empty \
                               "$(dirname "$weak_module")"
                    module_has_changed $module $krel
                fi
            done
        done
    fi
}

add_kernel() {
    add_krel=${1:-$(uname -r)}
    if [ ! -e "/boot/symvers-$add_krel.gz" ]; then
        echo "Symvers dump file /boot/symvers-$add_krel.gz" \
             "not found" >&2
        exit 1
    fi
    for krel in $(ls /lib/modules/ | rpmsort -r); do
        [ "$add_krel" = "$krel" ] && continue
        [ -d /lib/modules/$krel/extra ] || continue
        for module in $(find /lib/modules/$krel/extra -name '*.ko'); do
            subpath="${module#/lib/modules/$krel/extra}"
            weak_module="/lib/modules/$add_krel/weak-updates/${subpath#/}"
            [ -e "$weak_module" ] && continue
            if module_is_compatible $module $add_krel; then
                doit mkdir -p $(dirname $weak_module)
                doit ln -sf $module $weak_module
            fi
        done
    done
}

remove_kernel() {
    remove_krel=${1:-$(uname -r)}
    weak_modules="/lib/modules/$remove_krel/weak-updates"
    doit rm -rf "$weak_modules"
}

################################################################################
################################## MAIN GUTS ###################################
################################################################################

options=`getopt -o h --long help,add-modules,remove-modules \
                     --long add-kernel,remove-kernel,dry-run,verbose -- "$@"`

[ $? -eq 0 ] || usage 1

eval set -- "$options"

while :; do
    case "$1" in
    --add-modules)
        do_add_modules=1
        ;;
    --remove-modules)
        do_remove_modules=1
        ;;
    --add-kernel)
        do_add_kernel=1
        ;;
    --remove-kernel)
        do_remove_kernel=1
        ;;
    --dry-run)
        dry_run=1
        ;;
    --verbose)
        verbose=1
        ;;
    -h|--help)
        usage 0
        ;;
    --)
        shift
        break
        ;;
    esac
    shift
done

if [ -n "$do_add_modules" ]; then
	add_modules

elif [ -n "$do_remove_modules" ]; then
	remove_modules

elif [ -n "$do_add_kernel" ]; then
	kernel=${1:-$(uname -r)}
	add_kernel $kernel

elif [ -n "$do_remove_kernel" ]; then
	kernel=${1:-$(uname -r)}
	remove_kernel $kernel
else
	usage 1
fi

################################################################################
###################### CLEANUP POST ADD/REMOVE MODULE/KERNEL ###################
################################################################################

# run depmod and mkinitrd as needed
for krel in ${!changed_modules_*}; do
    krel=${!krel}

    doit /sbin/depmod -ae -F /boot/System.map-$krel $krel
done

for krel in ${!changed_initrd_*}; do
    krel=${!krel}

    doit /sbin/mkinitrd --allow-missing -f /boot/initrd-$krel.img $krel
done