#!/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_initramfs_*} initramfs_prefix="/boot" # can customize here dracut="/sbin/dracut" if [ ! -x "$dracut" ] then echo "weak-modules: this tool requires a dracut-enabled kernel" exit 1 fi # doit: # A wrapper used whenever we're going to perform a real operation. doit() { [ -n "$verbose" ] && echo "$@" [ -n "$dry_run" ] || "$@" } # 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() { local IFS=$' ' REVERSE="" rpmlist=($(cat)) if [ "-r" == "$1" ]; then REVERSE="-r" fi echo ${rpmlist[@]} | \ sed -e 's/-/../g' | \ sort ${REVERSE} -n -t"." -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 -k6,6 -k7,7 \ -k8,8 -k9,9 -k10,10 | \ sed -e 's/\.\./-/g' } # 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 } decompress_initramfs() { local input=$1 local output=$2 # First, check if this is compressed at all if cpio -i -t < "$input" > /dev/null 2>/dev/null; then # If this archive contains a file early_cpio, it's a trick. Strip off # the early cpio archive and try again. if cpio -i -t < "$input" 2>/dev/null | grep -q '^early_cpio$' ; then /usr/lib/dracut/skipcpio "$input" > "${tmpdir}/post_early_cpio.img" decompress_initramfs "${tmpdir}/post_early_cpio.img" "$output" retval="$?" rm -f "${tmpdir}/post_early_cpio.img" return $retval fi cp "$input" "$output" return 0 fi # Try gzip if gzip -cd < "$input" > "$output" 2>/dev/null ; then return 0 fi # Next try xz if xz -cd < "$input" > "$output" 2>/dev/null ; then return 0 fi echo "Unable to decompress $input: Unknown format" >&2 return 1 } # List all module files and modprobe configuration that could require a new # initramfs. The current directory must be the root of the uncompressed # initramfs. The unsorted list of files is output to stdout. list_module_files() { find . -iname \*.ko 2>/dev/null find etc/modprobe.d usr/lib/modprobe.d -name \*.conf 2>/dev/null } # read_old_initramfs: compare_initramfs_modules() { local old_initramfs=$1 local new_initramfs=$2 rm -rf "$tmpdir/old_initramfs" rm -rf "$tmpdir/new_initramfs" mkdir "$tmpdir/old_initramfs" mkdir "$tmpdir/new_initramfs" decompress_initramfs "$old_initramfs" "$tmpdir/old_initramfs.img" pushd "$tmpdir/old_initramfs" >/dev/null cpio -i < "$tmpdir/old_initramfs.img" 2>/dev/null rm "$tmpdir/old_initramfs.img" n=0; for i in `list_module_files|sort`; do old_initramfs_modules[n]="$i" n=$((n+1)) done popd >/dev/null decompress_initramfs "$new_initramfs" "$tmpdir/new_initramfs.img" pushd "$tmpdir/new_initramfs" >/dev/null cpio -i < "$tmpdir/new_initramfs.img" 2>/dev/null rm "$tmpdir/new_initramfs.img" n=0; for i in `list_module_files|sort`; do new_initramfs_modules[n]="$i" n=$((n+1)) done popd >/dev/null # Compare the length and contents of the arrays if [ "${#old_initramfs_modules[@]}" == "${#new_initramfs_modules[@]}" -a \ "${old_initramfs_modules[*]}" == "${new_initramfs_modules[*]}" ]; then # If the file lists are the same, compare each file to find any that changed for ((n = 0; n < ${#old_initramfs_modules[@]}; n++)); do if ! cmp "$tmpdir/old_initramfs/${old_initramfs_modules[n]}" \ "$tmpdir/new_initramfs/${new_initramfs_modules[n]}" ; then return 1 fi done else return 1 fi return 0 } # check_initramfs: # check and possibly also update the initramfs for changed kernels check_initramfs() { local kernel=$1 # If there is no initramfs already we will not make one here. if [ -e "$initramfs_prefix/initramfs-$kernel.img" ]; then old_initramfs="$initramfs_prefix/initramfs-$kernel.img" tmp_initramfs="$initramfs_prefix/initramfs-$kernel.tmp" new_initramfs="$initramfs_prefix/initramfs-$kernel.img" $dracut -f "$tmp_initramfs" "$kernel" if ! $(compare_initramfs_modules "$old_initramfs" "$tmp_initramfs"); then doit mv "$tmp_initramfs" "$new_initramfs" else rm -f "$tmp_initramfs" fi fi } # 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 kmods. 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 # Accept '.' in the symbol name to handle the .TOC. symbol on ppc if [ -e /boot/symvers-$krel.gz ]; then zcat /boot/symvers-$krel.gz \ | sed -r -ne 's:^(0x[0]*[0-9a-f]{8}\t[0-9a-zA-Z_.]+)\t.*:\1:p' fi > $tmpdir/symvers-$krel fi # Symbols exported by the add-on modules of the "new" kernel if [ ! -e "$tmpdir/addon-symvers-$krel" ]; then if [ -e /lib/modules/$krel/extra ] && \ [ -n "`find /lib/modules/$krel/extra -type f`" ]; then find /lib/modules/$krel/extra -name '*.ko' \ | xargs nm \ | sed -nre 's:^[0]*([0-9a-f]{8}) A __crc_(.*):0x\1 \2:p' fi > $tmpdir/addon-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 ] && \ [ -n "`find /lib/modules/$module_krel/extra -type f`" ]; then find /lib/modules/$module_krel/extra -name '*.ko' \ | xargs nm \ | sed -nre 's:^[0]*([0-9a-f]{8}) A __crc_(.*):0x\1 \2:p' fi > $tmpdir/extra-symvers-$module_krel fi sort -u $tmpdir/symvers-$krel \ $tmpdir/extra-symvers-$module_krel \ $tmpdir/addon-symvers-$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]*[0-9a-f]{8}\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 # , 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 } 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. Optionally specify --delete-modules to prevent weak-modules from attempting to locate any compatible modules to replace those being removed. --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. --no-initramfs Do not generate an initramfs. --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 mkinitramfs 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_initramfs_${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/*) # Module was built against this kernel, update initramfs. module_has_changed $module $krel continue ;; esac # Module my also serve as a weak-update built against another # kernel. We need to create symlinks for compatible kernels # under /lib/modules and rerun depmod/dracut for those. subpath=`echo $module | sed -nre "s:(/usr)?/lib/modules/$module_krel/([^/]*)/(.*):\3:p"` 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 | (read input; echo "$input"; \ while read input; do true; done))" = \ "$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 was built against another kernel, update initramfs. module_has_changed $module $krel fi done done fi } # remove_modules: # Read in a list of modules from stdinput and process them for removal. # Parameter is noreplace to delete modules, otherwise link compat. remove_modules() { delete_modules=${1:-replace} read_modules_list || exit 1 if [ ${#modules[@]} -gt 0 ]; then # Hunt for all known users of this module in /lib/modules, remove them # and create symlinks to other compatible modules (downgrade) if # possible, update initramfs for each modified kernel too. 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]}" # Module is going to be removed, update initramfs. module_has_changed $module $krel 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" if [ "replace" == "$delete_modules" ]; then for krel2 in "${krels[@]}"; do if [ $krel2 != $krel ]; then module="/lib/modules/$krel2/extra/${subpath#/}" [ -e "$module" ] || continue if module_is_compatible "$module" "$krel"; then [ -n "$verbose" ] && echo \ "Adding compatible module ${module##*/} from kernel $krel2 instead" doit ln -s "$module" "$weak_module" module_has_changed $module $krel break fi fi done fi # Remove the part of the directory beneath weak-updates ( cd "/lib/modules/$krel/weak-updates" && \ doit rmdir --parents --ignore-fail-on-non-empty "$(dirname "${subpath#/}")" ) 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 module_has_changed $module $add_krel 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" module_has_changed $weak_modules $remove_krel # Remove everything beneath the weak-updates directory ( cd "$weak_modules" && doit rm -rf * ) } ################################################################################ ################################## MAIN GUTS ################################### ################################################################################ options=`getopt -o h --long help,add-modules,remove-modules \ --long add-kernel,remove-kernel \ --long dry-run,no-initramfs,verbose,delete-modules -- "$@"` [ $? -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 ;; --no-initramfs) no_initramfs=1 ;; --verbose) verbose=1 ;; --delete-modules) do_delete_modules=1 ;; -h|--help) usage 0 ;; --) shift break ;; esac shift done if [ -n "$do_add_modules" ]; then add_modules elif [ -n "$do_remove_modules" ]; then if [ -n "$do_delete_modules" ]; then remove_modules "noreplace" else remove_modules fi 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 exit 0 else usage 1 fi ################################################################################ ###################### CLEANUP POST ADD/REMOVE MODULE/KERNEL ################### ################################################################################ # run depmod and dracut 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_initramfs_*}; do krel=${!krel} if [ ! -n "$no_initramfs" ]; then check_initramfs $krel fi done